diff --git a/core/fly_client.cpp b/core/fly_client.cpp index 8730656c21..de5a775366 100644 --- a/core/fly_client.cpp +++ b/core/fly_client.cpp @@ -1162,7 +1162,7 @@ void FlyClient::NetworkStd::Connection::OnMsg(ProofKernel2&& msg) if (!req.m_Msg.m_Fetch) ThrowUnexpected(); - if (req.m_Res.m_Kernel->IsValid(req.m_Res.m_Height)) + if (!req.m_Res.m_Kernel->IsValid(req.m_Res.m_Height)) ThrowUnexpected(); if (req.m_Res.m_Kernel->get_ID() != req.m_Msg.m_ID) diff --git a/mytest-utxo-image.bin b/mytest-utxo-image.bin new file mode 100644 index 0000000000..4bc2c7387c Binary files /dev/null and b/mytest-utxo-image.bin differ diff --git a/mytest.db b/mytest.db new file mode 100644 index 0000000000..98a072399e Binary files /dev/null and b/mytest.db differ diff --git a/receiver_wallet.db b/receiver_wallet.db new file mode 100644 index 0000000000..98580406a0 Binary files /dev/null and b/receiver_wallet.db differ diff --git a/sender_wallet.db b/sender_wallet.db new file mode 100644 index 0000000000..42ba61adc1 Binary files /dev/null and b/sender_wallet.db differ diff --git a/utility/cli/options.cpp b/utility/cli/options.cpp index 1ec70ae28d..909950063a 100644 --- a/utility/cli/options.cpp +++ b/utility/cli/options.cpp @@ -199,6 +199,7 @@ namespace beam const char* ESTIMATE_SWAP_FEERATE = "recommended_fee_rate"; const char* GET_BALANCE = "get_balance"; const char* SWAP_COIN = "swap_coin"; + const char* TOKEN_CONTRACT = "token_contract"; const char* SWAP_BEAM_SIDE = "swap_beam_side"; const char* SWAP_TX_HISTORY = "swap_tx_history"; const char* NODE_POLL_PERIOD = "node_poll_period"; @@ -528,7 +529,8 @@ namespace beam (cli::SWAP_WALLET_ADDR, po::value(), "rpc address of the swap wallet") (cli::SWAP_WALLET_USER, po::value(), "rpc user name for the swap wallet") (cli::SWAP_WALLET_PASS, po::value(), "rpc password for the swap wallet") - (cli::SWAP_COIN, po::value(), "swap coin currency (BTC/LTC/QTUM/DASH/DOGE/ETH)") + (cli::SWAP_COIN, po::value(), "swap coin currency (BTC/LTC/QTUM/DASH/DOGE/ETH/ERC20)") + (cli::TOKEN_CONTRACT, po::value(), "ERC-20 token contract address (0x + 40 hex chars), required when swap_coin=erc20") (cli::SWAP_AMOUNT, po::value>(), "swap amount in the smallest unit of the coin (e.g. satoshi for BTC)") (cli::SWAP_FEERATE, po::value>(), "specific feerate you are willing to pay (the smallest unit of the coin per KB)") (cli::SWAP_BEAM_SIDE, "should be always set by the swap party who owns BEAM") diff --git a/utility/cli/options.h b/utility/cli/options.h index 963e393d0d..034a02e8d3 100644 --- a/utility/cli/options.h +++ b/utility/cli/options.h @@ -167,6 +167,7 @@ namespace beam extern const char* ESTIMATE_SWAP_FEERATE; extern const char* GET_BALANCE; extern const char* SWAP_COIN; + extern const char* TOKEN_CONTRACT; extern const char* SWAP_BEAM_SIDE; extern const char* SWAP_TX_HISTORY; extern const char* NODE_POLL_PERIOD; diff --git a/wallet/api/cli/api_cli_swap.h b/wallet/api/cli/api_cli_swap.h index 445901f493..7f5001f502 100644 --- a/wallet/api/cli/api_cli_swap.h +++ b/wallet/api/cli/api_cli_swap.h @@ -63,8 +63,24 @@ class ApiCliSwap } private: + // Erc20Token is deliberately excluded from ethereum::IsEthereumBased (its + // contract is per-offer, not one of the fixed Dai/Usdt/WBTC entries with a + // static settings entry), but it still rides the same ethereum RPC + // connection as those coins for connectivity- and gas-price-related + // queries below. + [[nodiscard]] static bool usesEthConnection(AtomicSwapCoin swapCoin) + { + return ethereum::IsEthereumBased(swapCoin) || swapCoin == AtomicSwapCoin::Erc20Token; + } + [[nodiscard]] beam::Amount getCoinAvailable(AtomicSwapCoin swapCoin) const override { + if (swapCoin == AtomicSwapCoin::Erc20Token) + { + // Erc20Token has no single fixed contract; use getTokenAvailable instead. + return 0; + } + if (ethereum::IsEthereumBased(swapCoin)) { return _swapEthClient ? _swapEthClient->GetAvailable(swapCoin) : 0; @@ -75,9 +91,19 @@ class ApiCliSwap return swapClient ? swapClient->GetAvailable() : 0; } + [[nodiscard]] boost::optional getTokenAvailable(const std::string& tokenContract, uint8_t decimals) const override + { + if (!_swapEthClient) + { + return boost::none; + } + + return _swapEthClient->GetTokenAvailable(tokenContract, decimals); + } + [[nodiscard]] beam::Amount getRecommendedFeeRate(AtomicSwapCoin swapCoin) const override { - if (ethereum::IsEthereumBased(swapCoin)) + if (usesEthConnection(swapCoin)) { return _swapEthClient ? _swapEthClient->GetRecommendedFeeRate() : 0; } @@ -89,7 +115,7 @@ class ApiCliSwap [[nodiscard]] beam::Amount getMinFeeRate(AtomicSwapCoin swapCoin) const override { - if (ethereum::IsEthereumBased(swapCoin)) + if (usesEthConnection(swapCoin)) { return _swapEthClient ? _swapEthClient->GetSettings().GetMinFeeRate() : 0; } @@ -101,7 +127,7 @@ class ApiCliSwap [[nodiscard]] beam::Amount getMaxFeeRate(AtomicSwapCoin swapCoin) const override { - if (ethereum::IsEthereumBased(swapCoin)) + if (usesEthConnection(swapCoin)) { return _swapEthClient ? _swapEthClient->GetSettings().GetMaxFeeRate() : 0; } @@ -118,7 +144,7 @@ class ApiCliSwap [[nodiscard]] bool isCoinClientConnected(AtomicSwapCoin swapCoin) const override { - if (ethereum::IsEthereumBased(swapCoin)) + if (usesEthConnection(swapCoin)) { return _swapEthClient ? _swapEthClient->IsConnected() : 0; } diff --git a/wallet/api/cli/swap_eth_client.cpp b/wallet/api/cli/swap_eth_client.cpp index eb60479906..3d26f45d55 100644 --- a/wallet/api/cli/swap_eth_client.cpp +++ b/wallet/api/cli/swap_eth_client.cpp @@ -19,6 +19,9 @@ namespace { const unsigned int kBalanceUpdateInterval = 10 * 1000; // 10 seconds const unsigned int kPriceGasUpdateInterval = 60 * 1000; // 1 minute + // a watched token contract is polled until nobody has asked for its + // balance this long, then its watch and cache entries are dropped + constexpr std::chrono::minutes kTokenWatchIdleTimeout{10}; } SwapEthClient::SwapEthClient( @@ -53,6 +56,26 @@ Amount SwapEthClient::GetAvailable(beam::wallet::AtomicSwapCoin swapCoin) const return 0; } +boost::optional SwapEthClient::GetTokenAvailable(const std::string& tokenContract, uint8_t decimals) +{ + const auto now = std::chrono::steady_clock::now(); + auto [watched, isNew] = _watchedTokens.try_emplace(tokenContract, WatchedToken{decimals, now}); + watched->second.m_lastUse = now; + + auto iter = _tokenBalances.find(tokenContract); + if (iter != _tokenBalances.end()) + { + return iter->second; + } + + if (isNew && GetSettings().IsActivated()) + { + GetAsync()->GetTokenBalance(tokenContract, decimals); + } + + return boost::none; +} + Amount SwapEthClient::GetRecommendedFeeRate() const { return _recommendedFeeRate; @@ -74,6 +97,18 @@ void SwapEthClient::requestBalance() { GetAsync()->GetBalance(token); } + const auto deadline = std::chrono::steady_clock::now() - kTokenWatchIdleTimeout; + for (auto it = _watchedTokens.begin(); it != _watchedTokens.end();) + { + if (it->second.m_lastUse < deadline) + { + _tokenBalances.erase(it->first); + it = _watchedTokens.erase(it); + continue; + } + GetAsync()->GetTokenBalance(it->first, it->second.m_decimals); + ++it; + } } } @@ -96,6 +131,11 @@ void SwapEthClient::OnBalance(beam::wallet::AtomicSwapCoin swapCoin, beam::Amoun _balances[swapCoin] = balance; } +void SwapEthClient::OnTokenBalance(const std::string& tokenContract, beam::Amount balance) +{ + _tokenBalances[tokenContract] = balance; +} + void SwapEthClient::OnEstimatedGasPrice(Amount feeRate) { _recommendedFeeRate = feeRate; diff --git a/wallet/api/cli/swap_eth_client.h b/wallet/api/cli/swap_eth_client.h index 652414fa23..e56fa2e051 100644 --- a/wallet/api/cli/swap_eth_client.h +++ b/wallet/api/cli/swap_eth_client.h @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. #pragma once +#include +#include #include "wallet/transactions/swaps/bridges/ethereum/client.h" class SwapEthClient : public beam::ethereum::Client @@ -29,12 +31,19 @@ class SwapEthClient : public beam::ethereum::Client beam::Amount GetRecommendedFeeRate() const; bool IsConnected() const; + // Balance of an arbitrary ERC-20 contract, in wallet units for the given + // decimals. Returns boost::none until the first refresh cycle has answered + // for this contract; the contract is registered for polling as a side + // effect and dropped again once nobody has asked about it for a while. + boost::optional GetTokenAvailable(const std::string& tokenContract, uint8_t decimals); + private: void requestBalance(); void requestRecommendedFeeRate(); void OnStatus(Status status) override; void OnBalance(beam::wallet::AtomicSwapCoin swapCoin, beam::Amount balance) override; + void OnTokenBalance(const std::string& tokenContract, beam::Amount balance) override; void OnEstimatedGasPrice(beam::Amount feeRate) override; void OnCanModifySettingsChanged(bool canModify) override; void OnChangedSettings() override; @@ -44,6 +53,13 @@ class SwapEthClient : public beam::ethereum::Client beam::io::Timer::Ptr _timer; beam::io::Timer::Ptr _feeTimer; std::map _balances; + struct WatchedToken + { + uint8_t m_decimals; + std::chrono::steady_clock::time_point m_lastUse; + }; + std::map _watchedTokens; + std::map _tokenBalances; beam::Amount _recommendedFeeRate = 0; Status _status; }; diff --git a/wallet/api/i_swaps_provider.h b/wallet/api/i_swaps_provider.h index f50dbd0296..59c5455fc3 100644 --- a/wallet/api/i_swaps_provider.h +++ b/wallet/api/i_swaps_provider.h @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. #pragma once +#include #include "wallet/core/common.h" #include "wallet/transactions/swaps/common.h" @@ -25,10 +26,21 @@ namespace beam::wallet typedef std::shared_ptr Ptr; [[nodiscard]] virtual Amount getCoinAvailable(AtomicSwapCoin swapCoin) const = 0; + // Balance for an arbitrary per-offer ERC-20 contract, in wallet units for + // the given decimals. boost::none means the contract hasn't been observed + // long enough for a live query to have answered yet. + [[nodiscard]] virtual boost::optional getTokenAvailable(const std::string& tokenContract, uint8_t decimals) const = 0; [[nodiscard]] virtual Amount getRecommendedFeeRate(AtomicSwapCoin swapCoin) const = 0; [[nodiscard]] virtual Amount getMinFeeRate(AtomicSwapCoin swapCoin) const = 0; [[nodiscard]] virtual Amount getMaxFeeRate(AtomicSwapCoin swapCoin) const = 0; [[nodiscard]] virtual const SwapOffersBoard& getSwapOffersBoard() const = 0; [[nodiscard]] virtual bool isCoinClientConnected(AtomicSwapCoin swapCoin) const = 0; }; + + // True when a balance check should pass: either the balance isn't known yet + // (first time this contract is seen) or the known balance covers the total. + inline bool IsSwapAmountAvailable(const boost::optional& available, Amount total) + { + return !available || *available > total; + } } diff --git a/wallet/api/v6_0/v6_api_defs.h b/wallet/api/v6_0/v6_api_defs.h index dd783946e5..d0d0acf167 100644 --- a/wallet/api/v6_0/v6_api_defs.h +++ b/wallet/api/v6_0/v6_api_defs.h @@ -458,6 +458,21 @@ namespace beam::wallet Amount swapFeeRate = 0; Height offerLifetime = 15; std::string comment; + // Only meaningful when swapCoin == AtomicSwapCoin::Erc20Token: the + // per-offer ERC-20 contract address ("0x" + 40 hex chars) and the + // symbol()/decimals() the caller asserts for it. Unlike the CLI (which + // queries the contract live and asks for interactive confirmation), + // the API is not a live-query trust boundary, so the caller supplies + // these directly; they are re-validated (format + decimals bound) + // when the offer is created/accepted, and the wallet re-derives the + // authoritative values from the chain when the swap transaction runs. + std::string tokenContract; + std::string tokenSymbol; + uint8_t tokenDecimals = 0; + // Non-zero when the BEAM leg of the swap carries a Confidential Asset + // instead of plain BEAM; validated (known asset, sufficient balance) + // when the offer is created. + Asset::ID beamAssetId = Asset::s_InvalidID; }; struct OffersList diff --git a/wallet/api/v6_0/v6_api_swap_handle.cpp b/wallet/api/v6_0/v6_api_swap_handle.cpp index d1aa4b8dfd..9653e95c3f 100644 --- a/wallet/api/v6_0/v6_api_swap_handle.cpp +++ b/wallet/api/v6_0/v6_api_swap_handle.cpp @@ -19,8 +19,8 @@ #include "wallet/transactions/swaps/swap_transaction.h" #include "wallet/transactions/swaps/swap_tx_description.h" #include "wallet/transactions/swaps/utils.h" -#include #include "wallet/client/extensions/offers_board/swap_offers_board.h" +#include "wallet/transactions/swaps/common.h" namespace beam::wallet { @@ -44,9 +44,27 @@ namespace beam::wallet } } - bool checkIsEnoughtSwapAmount(const ISwapsProvider& swapProvider, AtomicSwapCoin swapCoin, Amount swapAmount, Amount swapFeeRate) + bool checkIsEnoughtSwapAmount(const ISwapsProvider& swapProvider, AtomicSwapCoin swapCoin, Amount swapAmount, Amount swapFeeRate, + const std::string& tokenContract, uint8_t tokenDecimals) { beam::Amount total = swapAmount + swapFeeRate; + + if (swapCoin == AtomicSwapCoin::Erc20Token) + { + // getCoinAvailable is keyed by a fixed AtomicSwapCoin and has no + // notion of a per-offer contract address; getTokenAvailable is the + // per-contract equivalent, backed by SwapEthClient's balance cache. + // A contract seen for the first time here has no cached balance yet + // (boost::none), so this check passes and an insufficient balance + // still fails when the lock tx is built; a later check on the same + // contract sees the live balance once the cache has been populated. + if (!IsValidEthContractAddress(tokenContract)) + { + return false; + } + return IsSwapAmountAvailable(swapProvider.getTokenAvailable(tokenContract, tokenDecimals), total); + } + return swapProvider.getCoinAvailable(swapCoin) > total; } @@ -248,12 +266,34 @@ namespace beam::wallet } else { - if(!checkIsEnoughtSwapAmount(*swaps, data.swapCoin, data.swapAmount, data.swapFeeRate)) + if(!checkIsEnoughtSwapAmount(*swaps, data.swapCoin, data.swapAmount, data.swapFeeRate, data.tokenContract, data.tokenDecimals)) { throw jsonrpc_exception(ApiError::InvalidJsonRpc, kSwapNotEnoughtSwapCoins); } } + std::string beamAssetUnitName; + if (data.beamAssetId) + { + const auto info = walletDB->findAsset(data.beamAssetId); + if (!info) + { + throw jsonrpc_exception(ApiError::InvalidParamsJsonRpc, + "Unknown 'beam_asset_id'. Receive the asset (or sync its info) before swapping it."); + } + beamAssetUnitName = WalletAssetMeta(*info).GetUnitName(); + + if (data.isBeamSide) + { + storage::Totals totals(*walletDB, false); + auto available = totals.GetTotals(data.beamAssetId).Avail; + if (AmountBig::get_Lo(available) < data.beamAmount) + { + throw jsonrpc_exception(ApiError::AssetSwapNotEnoughtFunds, "Not enough asset balance for the swap."); + } + } + } + auto txParameters = CreateSwapTransactionParameters(); auto currentHeight = walletDB->getCurrentHeight(); FillSwapTxParams( @@ -268,6 +308,19 @@ namespace beam::wallet data.isBeamSide, data.offerLifetime); + if (data.swapCoin == AtomicSwapCoin::Erc20Token) + { + txParameters.SetParameter(TxParameterID::AtomicSwapTokenContract, data.tokenContract); + txParameters.SetParameter(TxParameterID::AtomicSwapTokenSymbol, data.tokenSymbol); + txParameters.SetParameter(TxParameterID::AtomicSwapTokenDecimals, data.tokenDecimals); + } + + if (data.beamAssetId) + { + txParameters.SetParameter(TxParameterID::AtomicSwapBeamAssetID, data.beamAssetId); + txParameters.SetParameter(TxParameterID::AtomicSwapBeamAssetName, beamAssetUnitName); + } + if (!data.comment.empty()) { txParameters.SetParameter(TxParameterID::Message, @@ -396,6 +449,20 @@ namespace beam::wallet throw jsonrpc_exception(ApiError::SwapFailToParseToken, "bad or missing amounts or coins"); } + std::string erc20TokenContract; + uint8_t erc20TokenDecimals = 0; + if (*swapCoin == AtomicSwapCoin::Erc20Token) + { + // Defense-in-depth re-validation of a peer-supplied token: a token + // pasted directly via swap_accept_offer may not have gone through + // the offers board's own validation (isExtendedOfferDataValid). + std::string erc20TokenSymbol; + if (!GetValidatedErc20Params(*txParams, erc20TokenContract, erc20TokenSymbol, erc20TokenDecimals)) + { + throw jsonrpc_exception(ApiError::SwapFailToParseToken, "invalid or missing ERC-20 token parameters"); + } + } + Amount recommendedFeeRate = swaps->getRecommendedFeeRate(*swapCoin); if (recommendedFeeRate > 0 && data.swapFeeRate < recommendedFeeRate) @@ -439,10 +506,24 @@ namespace beam::wallet } else { - if(!checkIsEnoughtSwapAmount(*swaps, *swapCoin, *swapAmount, data.swapFeeRate)) + if(!checkIsEnoughtSwapAmount(*swaps, *swapCoin, *swapAmount, data.swapFeeRate, erc20TokenContract, erc20TokenDecimals)) { throw jsonrpc_exception(InvalidJsonRpc, kSwapNotEnoughtSwapCoins); } + + // !*isBeamSide means this wallet gives the swap coin and receives + // BEAM (and, when the offer carries one, the Confidential Asset + // riding on the BEAM leg). The redeem tx's fee is paid in BEAM. + if (auto beamAssetId = txParams->GetParameter(TxParameterID::AtomicSwapBeamAssetID); + beamAssetId && *beamAssetId != Asset::s_InvalidID) + { + storage::Totals totals(*walletDB, false); + if (AmountBig::get_Lo(totals.GetBeamTotals().Avail) <= data.beamFee) + { + throw jsonrpc_exception(ApiError::SwapNotEnoughtBeams, + "Receiving an asset via swap requires a BEAM balance for the redeem fee."); + } + } } SwapOffer offer = SwapOffer(*txParams); @@ -567,4 +648,4 @@ namespace beam::wallet BEAM_LOG_DEBUG() << "CancelOffer(txId = " << to_hex(data.txId.data(), data.txId.size()) << ")"; onHandleTxCancel(id, std::move(data)); } -} \ No newline at end of file +} diff --git a/wallet/api/v6_0/v6_api_swap_parse.cpp b/wallet/api/v6_0/v6_api_swap_parse.cpp index 7b615ec8bf..58ccc4408f 100644 --- a/wallet/api/v6_0/v6_api_swap_parse.cpp +++ b/wallet/api/v6_0/v6_api_swap_parse.cpp @@ -38,6 +38,39 @@ namespace beam::wallet return result; } + // Adds the extended-offer fields (only present when the offer actually + // carries them: a per-offer ERC-20 token and/or a non-BEAM Beam-side + // asset) to an offer/token json result. Mirrors the CLI's printout in + // AcceptSwap (wallet/cli/swaps.cpp). + void addExtendedOfferFieldsToJson(json& result, const SwapOffer& offer) + { + if (offer.ResolveCoin() == AtomicSwapCoin::Erc20Token) + { + if (auto contract = offer.GetParameter(TxParameterID::AtomicSwapTokenContract)) + { + result["token_contract"] = *contract; + } + if (auto symbol = offer.GetParameter(TxParameterID::AtomicSwapTokenSymbol)) + { + result["token_symbol"] = *symbol; + } + if (auto decimals = offer.GetParameter(TxParameterID::AtomicSwapTokenDecimals)) + { + result["token_decimals"] = static_cast(*decimals); + } + } + + if (auto beamAssetId = offer.GetParameter(TxParameterID::AtomicSwapBeamAssetID); + beamAssetId && *beamAssetId != Asset::s_InvalidID) + { + result["beam_asset_id"] = *beamAssetId; + if (auto assetName = offer.GetParameter(TxParameterID::AtomicSwapBeamAssetName)) + { + result["beam_asset_unit_name"] = *assetName; + } + } + } + json TokenToJson(const SwapOffer& offer, bool isMyOffer = false, bool isPublic = false) { // TODO roman.strilets: check isPublic in this code!!! @@ -71,6 +104,8 @@ namespace beam::wallet {"time_created", createTimeStr}, }; + addExtendedOfferFieldsToJson(result, offer); + return result; } @@ -122,6 +157,8 @@ namespace beam::wallet {"height_expired", expiredHeight}, }; + addExtendedOfferFieldsToJson(result, offer); + if (offer.m_status == SwapOfferStatus::Pending) { result["is_my_offer"] = isOwnOffer; @@ -149,6 +186,50 @@ namespace beam::wallet throw jsonrpc_exception(ApiError::InvalidJsonRpc, message); } + // token_contract (mandatory), token_symbol (mandatory), token_decimals + // (mandatory, bounded by kMaxTokenDecimals) are only meaningful for + // AtomicSwapCoin::Erc20Token; parsed/validated here once for both + // create-offer and accept-offer callers. + void readErc20TokenParams(const JsonRpcId& id, const json& params, OfferInput& data) + { + if (data.swapCoin != AtomicSwapCoin::Erc20Token) + { + return; + } + + const std::string tokenContract = V6Api::getMandatoryParam(params, "token_contract"); + if (!IsValidEthContractAddress(tokenContract)) + { + throw jsonrpc_exception(ApiError::InvalidParamsJsonRpc, + "'token_contract' is not a valid ERC-20 contract address (expected '0x' followed by 40 hex characters)."); + } + + const std::string tokenSymbol = V6Api::getMandatoryParam(params, "token_symbol"); + + const auto tokenDecimals = V6Api::getMandatoryParam(params, "token_decimals"); + if (tokenDecimals > kMaxTokenDecimals) + { + throw jsonrpc_exception(ApiError::InvalidParamsJsonRpc, + "'token_decimals' exceeds the maximum supported value."); + } + + data.tokenContract = tokenContract; + data.tokenSymbol = tokenSymbol; + data.tokenDecimals = static_cast(tokenDecimals); + } + + // beam_asset_id (optional): when present and non-zero, the BEAM leg of + // the offer carries a Confidential Asset instead of plain BEAM. Full + // validation (the wallet knows the asset, holds enough of it) happens + // in onHandleCreateOffer, once the wallet DB is available. + void readBeamAssetParam(const json& params, OfferInput& data) + { + if (auto assetId = V6Api::getOptionalParam(params, "beam_asset_id")) + { + data.beamAssetId = *assetId; + } + } + Amount readSwapFeeRateParameter(const JsonRpcId& id, const json& params) { return V6Api::getMandatoryParam(params, "fee_rate"); @@ -239,6 +320,9 @@ namespace beam::wallet data.swapAmount = data.isBeamSide ? receiveAmount : sendAmount; data.beamFee = V6Api::getBeamFeeParam(params, "beam_fee"); + readErc20TokenParams(id, params, data); + readBeamAssetParam(params, data); + if (data.isBeamSide && data.beamAmount < data.beamFee) { throw jsonrpc_exception(ApiError::InvalidParamsJsonRpc, "beam swap amount is less than (default) fee."); diff --git a/wallet/cli/swaps.cpp b/wallet/cli/swaps.cpp index c499147c56..5dd6966d56 100644 --- a/wallet/cli/swaps.cpp +++ b/wallet/cli/swaps.cpp @@ -44,7 +44,6 @@ #include #include #include -#include using namespace std; using namespace beam; @@ -57,7 +56,32 @@ namespace { const char kElectrumSeparateSymbol = ' '; -Amount ReadEthSwapAmount(const po::variables_map& vm, AtomicSwapCoin swapCoin) +// Erc20Token has no fixed entry in UnitsPerCoin (it asserts for this +// pseudo-coin); its units are derived from the per-offer decimals() +// queried from the token contract instead. +uint64_t EthUnitsPerCoin(AtomicSwapCoin swapCoin, boost::optional tokenDecimals) +{ + return (swapCoin == AtomicSwapCoin::Erc20Token) + ? ethereum::WalletUnitsPerToken(tokenDecimals.value_or(0)) + : UnitsPerCoin(swapCoin); +} + +bool ConfirmYesNo(const char* prompt) +{ + while (true) + { + std::string result; + cout << prompt << endl; + cin >> result; + + if (result == "y" || result == "n") + { + return result == "y"; + } + } +} + +Amount ReadEthSwapAmount(const po::variables_map& vm, AtomicSwapCoin swapCoin, boost::optional tokenDecimals = boost::none) { if (vm.count(cli::ETH_SWAP_AMOUNT) == 0) { @@ -69,8 +93,8 @@ Amount ReadEthSwapAmount(const po::variables_map& vm, AtomicSwapCoin swapCoin) try { boost::multiprecision::cpp_dec_float_50 preciseAmount(strAmount); - - preciseAmount *= UnitsPerCoin(swapCoin); + + preciseAmount *= EthUnitsPerCoin(swapCoin, tokenDecimals); return preciseAmount.convert_to(); } @@ -90,15 +114,15 @@ Amount ReadGasPrice(const po::variables_map& vm) return vm[cli::ETH_GAS_PRICE].as>().value; } -std::string PrintEth(beam::Amount value, AtomicSwapCoin swapCoin) +std::string PrintEth(beam::Amount value, AtomicSwapCoin swapCoin, boost::optional tokenDecimals = boost::none, const std::string& tokenSymbol = {}) { const uint64_t unitsToPrint = 1'000'000u; boost::multiprecision::cpp_dec_float_50 preciseAmount(value); - auto unitsPerCoin = UnitsPerCoin(swapCoin); + const uint64_t unitsPerCoin = EthUnitsPerCoin(swapCoin, tokenDecimals); if (unitsPerCoin > unitsToPrint) { - preciseAmount /= UnitsPerCoin(swapCoin) / unitsToPrint; + preciseAmount /= unitsPerCoin / unitsToPrint; } preciseAmount = boost::multiprecision::round(preciseAmount); @@ -112,7 +136,11 @@ std::string PrintEth(beam::Amount value, AtomicSwapCoin swapCoin) preciseAmount /= unitsToPrint; } - return preciseAmount.str() + " " + std::to_string(swapCoin); + const std::string suffix = (swapCoin == AtomicSwapCoin::Erc20Token && !tokenSymbol.empty()) + ? tokenSymbol + : std::to_string(swapCoin); + + return preciseAmount.str() + " " + suffix; } template @@ -636,7 +664,10 @@ Amount EstimateSwapFeerate(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) { Amount result = 0; - if (ethereum::IsEthereumBased(swapCoin)) + // Erc20Token is not IsEthereumBased (its contract is per-offer, not one of + // the fixed coins that macro covers), but its fee is still paid in ETH gas, + // so it takes the same estimation path as the fixed ethereum-based coins. + if (ethereum::IsEthereumBased(swapCoin) || swapCoin == AtomicSwapCoin::Erc20Token) { auto callback = [&result](ethereum::IBridge::Ptr bridge) { @@ -706,7 +737,11 @@ Amount GetMinSwapFeeRate(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) case AtomicSwapCoin::Dai: case AtomicSwapCoin::Usdt: case AtomicSwapCoin::WBTC: + case AtomicSwapCoin::Erc20Token: { + // Gas price bounds are coin-agnostic (paid in ETH regardless of the + // ERC-20 token being swapped), so Erc20Token shares the ethereum + // settings provider with the fixed ethereum-based coins. return GetMinSwapFeeRate(walletDB); } default: @@ -751,6 +786,7 @@ Amount GetMaxSwapFeeRate(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) case AtomicSwapCoin::Dai: case AtomicSwapCoin::Usdt: case AtomicSwapCoin::WBTC: + case AtomicSwapCoin::Erc20Token: { return GetMaxSwapFeeRate(walletDB); } @@ -762,15 +798,24 @@ Amount GetMaxSwapFeeRate(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) } } -Amount GetBalance(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) +Amount GetBalance(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB, const std::string& erc20TokenContract, uint8_t erc20TokenDecimals) { Amount result = 0; - if (ethereum::IsEthereumBased(swapCoin)) + const bool isErc20 = (swapCoin == AtomicSwapCoin::Erc20Token); + + if (ethereum::IsEthereumBased(swapCoin) || isErc20) { - auto callback = [&result, swapCoin, walletDB](beam::ethereum::IBridge::Ptr bridge) + // Erc20Token's units-per-coin is derived from the per-offer decimals + // (there is no fixed table entry for it), unlike the classic + // ethereum-based coins whose multiplier is a fixed constant. + const uint32_t unitsMultiplier = isErc20 + ? ethereum::TokenUnitsMultiplier(erc20TokenDecimals) + : ethereum::GetCoinUnitsMultiplier(swapCoin); + + auto callback = [&result, swapCoin, walletDB, isErc20, &erc20TokenContract, unitsMultiplier](beam::ethereum::IBridge::Ptr bridge) { - auto balanceCallback = [&result, swapCoin](const ethereum::IBridge::Error& error, const std::string& balance) + auto balanceCallback = [&result, unitsMultiplier](const ethereum::IBridge::Error& error, const std::string& balance) { if (error.m_type != ethereum::IBridge::ErrorType::None) { @@ -779,7 +824,7 @@ Amount GetBalance(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) boost::multiprecision::uint256_t tmp(balance); - tmp /= ethereum::GetCoinUnitsMultiplier(swapCoin); + tmp /= unitsMultiplier; result = tmp.convert_to(); io::Reactor::get_Current().stop(); @@ -789,6 +834,15 @@ Amount GetBalance(AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB) { bridge->getBalance(balanceCallback); } + else if (isErc20) + { + if (erc20TokenContract.empty()) + { + throw std::runtime_error("Token contract is absent"); + } + + bridge->getTokenBalance(erc20TokenContract, balanceCallback); + } else { ethereum::SettingsProvider settingsProvider(walletDB); @@ -843,10 +897,71 @@ boost::optional InitSwap(const po::variables_map& vm, const IWalletDB::Ptr swapCoin = wallet::from_string(vm[cli::SWAP_COIN].as()); } + const bool isErc20 = (swapCoin == AtomicSwapCoin::Erc20Token); + + std::string tokenContract; + std::string tokenSymbol; + uint8_t tokenDecimals = 0; + + if (isErc20) + { + if (vm.count(cli::TOKEN_CONTRACT) == 0) + { + throw std::runtime_error("token_contract should be specified for swap_coin=erc20"); + } + + tokenContract = vm[cli::TOKEN_CONTRACT].as(); + if (!IsValidEthContractAddress(tokenContract)) + { + throw std::runtime_error("token_contract is not a valid ERC-20 contract address (expected '0x' followed by 40 hex characters)."); + } + + // Fetch symbol()/decimals() from the contract before parsing + // eth_swap_amount below: the amount is denominated in the token's + // own decimals, not a fixed table, so the wallet must know them first. + ethereum::IBridge::Error tokenInfoError{ ethereum::IBridge::ErrorType::None, "" }; + RequestToEthBridge(walletDB, [&](ethereum::IBridge::Ptr bridge) + { + bridge->getTokenInfo(tokenContract, + [&](const ethereum::IBridge::Error& error, const std::string& symbol, uint8_t decimals) + { + tokenInfoError = error; + tokenSymbol = symbol; + tokenDecimals = decimals; + io::Reactor::get_Current().stop(); + }); + + io::Reactor::get_Current().run(); + }); + + if (tokenInfoError.m_type != ethereum::IBridge::ErrorType::None) + { + throw std::runtime_error("failed to query the ERC-20 token contract: " + tokenInfoError.m_message); + } + + // Belt-and-braces: EthereumBridge::getTokenInfo already rejects + // decimals() > kMaxTokenDecimals, but the CLI is itself a trust + // boundary for whatever the counterparty-controlled contract returned. + if (tokenDecimals > wallet::kMaxTokenDecimals) + { + throw std::runtime_error("token decimals() exceeds the maximum supported value."); + } + + cout << " ERC-20 token contract: " << tokenContract << "\n" + << " Token symbol: " << tokenSymbol << "\n" + << " Token decimals: " << static_cast(tokenDecimals) << "\n" << endl; + + if (!ConfirmYesNo("Do you agree to swap this ERC-20 token? (y/n): ")) + { + BEAM_LOG_INFO() << "Swap rejected!"; + return boost::none; + } + } + Amount swapAmount = 0; Amount swapFeeRate = 0; - if (ethereum::IsEthereumBased(swapCoin)) + if (ethereum::IsEthereumBased(swapCoin) || isErc20) { if (vm.count(cli::ETH_SWAP_AMOUNT) == 0) { @@ -858,7 +973,7 @@ boost::optional InitSwap(const po::variables_map& vm, const IWalletDB::Ptr throw std::runtime_error("eth_gas_price should be specified"); } - swapAmount = ReadEthSwapAmount(vm, swapCoin); + swapAmount = isErc20 ? ReadEthSwapAmount(vm, swapCoin, tokenDecimals) : ReadEthSwapAmount(vm, swapCoin); swapFeeRate = ReadGasPrice(vm); if (!swapAmount) @@ -933,6 +1048,15 @@ boost::optional InitSwap(const po::variables_map& vm, const IWalletDB::Ptr bool isBeamSide = (vm.count(cli::SWAP_BEAM_SIDE) != 0); + if (!isBeamSide) + { + Amount balance = GetBalance(swapCoin, walletDB, tokenContract, tokenDecimals); + if (swapAmount > balance) + { + throw std::runtime_error("The swap amount must not exceed the " + GetCoinName(swapCoin) + " balance."); + } + } + Asset::ID assetId = Asset::s_InvalidID; Amount amount = 0; Amount fee = 0; @@ -943,12 +1067,41 @@ boost::optional InitSwap(const po::variables_map& vm, const IWalletDB::Ptr return boost::none; } + std::string assetUnitName; if (assetId) { - throw std::runtime_error(kErrorCantSwapAsset); + // The wallet must know the asset and hold enough of it. + const auto info = walletDB->findAsset(assetId); + if (!info) + { + throw std::runtime_error("Unknown asset id. Receive the asset (or sync its info) before swapping it."); + } + assetUnitName = WalletAssetMeta(*info).GetUnitName(); + + storage::Totals totals(*walletDB, false); + auto availableAsset = totals.GetTotals(assetId).Avail; + if (AmountBig::get_Lo(availableAsset) < amount) + { + throw std::runtime_error("Not enough asset balance for the swap."); + } + + // The fee is paid in BEAM, separately from the asset amount. + if (fee == 0) + { + throw std::runtime_error("Fee must be greater than zero."); + } + auto availableBeam = totals.GetTotals(Asset::s_BeamID).Avail; + if (AmountBig::get_Lo(availableBeam) < fee) + { + throw std::runtime_error("Not enough BEAM balance to pay the transaction fee."); + } + + // The counterparty redeeming this asset needs BEAM too. + cout << "Note: the counterparty will need a small BEAM balance to pay the " + "redeem transaction fee when claiming this asset." << endl; } - if (amount <= fee) + if (!assetId && amount <= fee) { throw std::runtime_error(kErrorSwapAmountTooLow); } @@ -968,6 +1121,19 @@ boost::optional InitSwap(const po::variables_map& vm, const IWalletDB::Ptr swapFeeRate, isBeamSide); + if (isErc20) + { + swapTxParameters.SetParameter(TxParameterID::AtomicSwapTokenContract, tokenContract); + swapTxParameters.SetParameter(TxParameterID::AtomicSwapTokenSymbol, tokenSymbol); + swapTxParameters.SetParameter(TxParameterID::AtomicSwapTokenDecimals, tokenDecimals); + } + + if (assetId) + { + swapTxParameters.SetParameter(TxParameterID::AtomicSwapBeamAssetID, assetId); + swapTxParameters.SetParameter(TxParameterID::AtomicSwapBeamAssetName, assetUnitName); + } + boost::optional currentTxID = wallet.StartTransaction(swapTxParameters); // print swap tx token @@ -1009,9 +1175,25 @@ boost::optional AcceptSwap(const po::variables_map& vm, const IWalletDB::P throw std::runtime_error("swap transaction token is invalid."); } + const bool isErc20 = (*swapCoin == AtomicSwapCoin::Erc20Token); + std::string tokenContract; + std::string tokenSymbol; + uint8_t tokenDecimals = 0; + + auto beamAssetId = swapTxParameters->GetParameter(TxParameterID::AtomicSwapBeamAssetID); + const bool hasBeamAsset = beamAssetId && *beamAssetId != Asset::s_InvalidID; + + if (isErc20) + { + if (!GetValidatedErc20Params(*swapTxParameters, tokenContract, tokenSymbol, tokenDecimals)) + { + throw std::runtime_error("swap transaction token carries invalid ERC-20 token parameters."); + } + } + Amount swapFeeRate = 0; - if (ethereum::IsEthereumBased(*swapCoin)) + if (ethereum::IsEthereumBased(*swapCoin) || isErc20) { if (vm.count(cli::ETH_GAS_PRICE) == 0) { @@ -1084,6 +1266,27 @@ boost::optional AcceptSwap(const po::variables_map& vm, const IWalletDB::P Amount fee = 0; ReadFee(vm, fee, wallet); + if (!*isBeamSide) + { + Amount balance = GetBalance(*swapCoin, walletDB, tokenContract, tokenDecimals); + if (*swapAmount > balance) + { + throw std::runtime_error("The swap amount must not exceed the " + GetCoinName(*swapCoin) + " balance."); + } + + // !*isBeamSide means this wallet gives the swap coin and receives BEAM + // (and, when hasBeamAsset, the asset riding on the BEAM leg). The + // redeem tx's fee is paid in BEAM regardless of the asset. + if (hasBeamAsset) + { + storage::Totals totals(*walletDB, false); + if (AmountBig::get_Lo(totals.GetBeamTotals().Avail) <= fee) + { + throw std::runtime_error("Receiving an asset via swap requires a BEAM balance for the redeem fee."); + } + } + } + ProcessLibraryVersion(*swapTxParameters); // display swap details to user @@ -1091,27 +1294,36 @@ boost::optional AcceptSwap(const po::variables_map& vm, const IWalletDB::P << " Beam side: " << *isBeamSide << "\n" << " Swap coin: " << to_string(*swapCoin) << "\n" << " Beam amount: " << PrintableAmount(*beamAmount) << "\n" - << " Swap amount: " << (ethereum::IsEthereumBased(*swapCoin) ? PrintEth(*swapAmount, *swapCoin): std::to_string(*swapAmount)) << "\n" - << " Peer ID: " << to_string(*peerID) << "\n" - << " Fee: " << PrintableAmount(fee) << "\n" << endl; - - // get accepting - // TODO: Refactor - bool isAccepted = false; - while (true) + << " Swap amount: " << ((ethereum::IsEthereumBased(*swapCoin) || isErc20) + ? PrintEth(*swapAmount, *swapCoin, isErc20 ? boost::make_optional(tokenDecimals) : boost::none, tokenSymbol) + : std::to_string(*swapAmount)) << "\n"; + if (isErc20) { - std::string result; - cout << "Do you agree to these conditions? (y/n): " << endl; - cin >> result; + cout << " Token contract: " << tokenContract << "\n" + << " Token symbol: " << tokenSymbol << "\n" + << " Token decimals: " << static_cast(tokenDecimals) << "\n"; + } + if (hasBeamAsset) + { + auto assetName = swapTxParameters->GetParameter(TxParameterID::AtomicSwapBeamAssetName); + cout << " Beam-side asset id: " << *beamAssetId << "\n" + << " Asset unit name: " << (assetName ? *assetName : std::string("")) << "\n" + << " NOTE: this swap moves a Confidential Asset on the BEAM side. You must\n" + << " hold BEAM to pay the redeem transaction fee.\n"; + } + cout << " Peer ID: " << to_string(*peerID) << "\n" + << " Fee: " << PrintableAmount(fee) << "\n" << endl; - if (result == "y" || result == "n") + if (hasBeamAsset) + { + if (!ConfirmYesNo("Do you agree to swap this Confidential Asset? (y/n): ")) { - isAccepted = (result == "y"); - break; + BEAM_LOG_INFO() << "Swap rejected!"; + return boost::none; } } - if (!isAccepted) + if (!ConfirmYesNo("Do you agree to these conditions? (y/n): ")) { BEAM_LOG_INFO() << "Swap rejected!"; return boost::none; @@ -1162,6 +1374,7 @@ int SetSwapSettings(const po::variables_map& vm, const IWalletDB::Ptr& walletDB, case AtomicSwapCoin::Dai: case AtomicSwapCoin::Usdt: case AtomicSwapCoin::WBTC: + case AtomicSwapCoin::Erc20Token: { return SetEthSettings(vm, walletDB, swapCoin); } @@ -1212,6 +1425,7 @@ void ShowSwapSettings(const po::variables_map& vm, const IWalletDB::Ptr& walletD case AtomicSwapCoin::Dai: case AtomicSwapCoin::Usdt: case AtomicSwapCoin::WBTC: + case AtomicSwapCoin::Erc20Token: { ShowEthSettings(walletDB); break; diff --git a/wallet/cli/swaps.h b/wallet/cli/swaps.h index 50e51f3f4e..aaf9a72cb0 100644 --- a/wallet/cli/swaps.h +++ b/wallet/cli/swaps.h @@ -24,12 +24,13 @@ #include "wallet/transactions/swaps/common.h" namespace beam::wallet -{ +{ boost::optional InitSwap(const po::variables_map& vm, const IWalletDB::Ptr& walletDB, Wallet& wallet); boost::optional AcceptSwap(const po::variables_map& vm, const IWalletDB::Ptr& walletDB, Wallet& wallet); bool HasActiveSwapTx(const IWalletDB::Ptr& walletDB, AtomicSwapCoin swapCoin); Amount EstimateSwapFeerate(beam::wallet::AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB); - Amount GetBalance(beam::wallet::AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB); + Amount GetBalance(beam::wallet::AtomicSwapCoin swapCoin, IWalletDB::Ptr walletDB, + const std::string& erc20TokenContract = {}, uint8_t erc20TokenDecimals = 0); int SetSwapSettings(const po::variables_map& vm, const IWalletDB::Ptr& walletDB, AtomicSwapCoin swapCoin); void ShowSwapSettings(const po::variables_map& vm, const IWalletDB::Ptr& walletDB, AtomicSwapCoin swapCoin); -} // beam::wallet \ No newline at end of file +} // beam::wallet diff --git a/wallet/client/extensions/offers_board/swap_offer.cpp b/wallet/client/extensions/offers_board/swap_offer.cpp index 81c565493b..a06005eff4 100644 --- a/wallet/client/extensions/offers_board/swap_offer.cpp +++ b/wallet/client/extensions/offers_board/swap_offer.cpp @@ -125,6 +125,28 @@ bool SwapOffer::IsValid() const && amount && swapAmount && responseTime && minimalHeight && txId; } +bool SwapOffer::IsExtended() const +{ + auto beamAssetId = GetParameter(TxParameterID::AtomicSwapBeamAssetID); + if (beamAssetId.value_or(0) != 0) + { + return true; + } + + auto paramCoin = GetParameter(TxParameterID::AtomicSwapCoin); + return m_coin == AtomicSwapCoin::Erc20Token || + (paramCoin && *paramCoin == AtomicSwapCoin::Erc20Token); +} + +AtomicSwapCoin SwapOffer::ResolveCoin() const +{ + if (m_coin != AtomicSwapCoin::ExtendedOffer) + { + return m_coin; + } + return GetParameter(TxParameterID::AtomicSwapCoin).value_or(AtomicSwapCoin::Unknown); +} + bool SwapOffer::isBeamSide() const { bool res = true; diff --git a/wallet/client/extensions/offers_board/swap_offer.h b/wallet/client/extensions/offers_board/swap_offer.h index faa9c65838..aaf53f39b9 100644 --- a/wallet/client/extensions/offers_board/swap_offer.h +++ b/wallet/client/extensions/offers_board/swap_offer.h @@ -36,6 +36,15 @@ struct SwapOffer : public TxParameters bool IsValid() const; + // True when this offer carries extended-offer data: a non-BEAM asset on the + // Beam side, or an ERC-20 token as the foreign coin (wire/param coin == + // AtomicSwapCoin::Erc20Token). + bool IsExtended() const; + // The real foreign coin. When m_coin == ExtendedOffer (the wire placeholder + // substituted for old-wallet compatibility), resolves the actual coin + // stashed in the AtomicSwapCoin tx parameter. + AtomicSwapCoin ResolveCoin() const; + bool isBeamSide() const; Amount amountBeam() const; Amount amountSwapCoin() const; diff --git a/wallet/client/extensions/offers_board/swap_offers_board.cpp b/wallet/client/extensions/offers_board/swap_offers_board.cpp index f7ae2808ff..1079bd7cb7 100644 --- a/wallet/client/extensions/offers_board/swap_offers_board.cpp +++ b/wallet/client/extensions/offers_board/swap_offers_board.cpp @@ -16,8 +16,27 @@ #include "utility/logger.h" +#include +#include + namespace beam::wallet { +namespace +{ + // Token symbol as reported by a peer's ERC-20 contract: bounded length, + // printable ASCII only (0x20-0x7E), never empty. + bool isValidTokenSymbol(const std::string& symbol) + { + if (symbol.empty() || symbol.size() > 32) + { + return false; + } + return std::all_of(symbol.begin(), symbol.end(), [](unsigned char c) + { + return c >= 0x20 && c <= 0x7E; + }); + } +} // namespace /** * @broadcastRouter incoming messages source * @messageEndpoint outgoing messages destination @@ -198,12 +217,33 @@ void SwapOffersBoard::fillOwnAdresses() bool SwapOffersBoard::onOfferFromNetwork(SwapOffer& newOffer) { - if (newOffer.m_coin >= AtomicSwapCoin::Unknown || newOffer.m_status > SwapOfferStatus::Failed) + // ExtendedOffer (the wire placeholder for asset/ERC-20 offers) is + // accepted and validated below; only true unknowns are rejected outright. + // Erc20Token must never appear as the top-level wire coin: a legitimate + // publisher always substitutes ExtendedOffer on the wire and carries the + // real Erc20Token coin inside the AtomicSwapCoin tx param (see + // SwapOffersBoard::broadcastOffer / SwapOffer::IsExtended). A raw wire + // Erc20Token is therefore malformed by definition and rejected outright. + if (newOffer.m_coin > AtomicSwapCoin::ExtendedOffer || + newOffer.m_coin == AtomicSwapCoin::Erc20Token || + newOffer.m_status > SwapOfferStatus::Failed) { BEAM_LOG_WARNING() << "offer board message is invalid"; return false; } + if (newOffer.m_coin == AtomicSwapCoin::ExtendedOffer) + { + if (!isExtendedOfferDataValid(newOffer)) + { + BEAM_LOG_WARNING() << "offer board message is invalid"; + return false; + } + // ExtendedOffer is only a wire placeholder (kept old wallets from + // mis-parsing the coin); internally we always work with the real coin. + newOffer.m_coin = newOffer.ResolveCoin(); + } + auto it = m_offersCache.find(newOffer.m_txId); if (it == m_offersCache.end()) // New offer @@ -245,10 +285,13 @@ bool SwapOffersBoard::onOfferFromNetwork(SwapOffer& newOffer) { if (newOffer.m_status == SwapOfferStatus::Pending && isOwnOffer(newOffer)) { - // fill missing parameters and send stored status to network - existingOffer.m_coin = newOffer.m_coin; - existingOffer.m_publisherId = newOffer.m_publisherId; - existingOffer.m_isOwn = true; + // the incomplete cache entry carries only txId + status; adopt + // the full parameter set from the network copy so the update + // broadcast is valid (extended offers need the packed + // coin/token params on the wire), then send the stored status + newOffer.m_status = existingOffer.m_status; + newOffer.m_isOwn = true; + existingOffer = newOffer; sendUpdateToNetwork(existingOffer); } } @@ -290,6 +333,45 @@ bool SwapOffersBoard::isOfferLifetimeTooLong(const SwapOffer& offer) const else return true; } +/** + * Validates the payload of an incoming offer wire-tagged as ExtendedOffer: + * the real foreign coin must resolve and not be Unknown, and the params + * required to interpret it must be present. + */ +bool SwapOffersBoard::isExtendedOfferDataValid(const SwapOffer& offer) const +{ + auto resolvedCoin = offer.ResolveCoin(); + if (resolvedCoin == AtomicSwapCoin::Unknown || resolvedCoin == AtomicSwapCoin::ExtendedOffer) + { + return false; + } + + if (resolvedCoin == AtomicSwapCoin::Erc20Token) + { + auto decimals = offer.GetParameter(TxParameterID::AtomicSwapTokenDecimals); + auto contract = offer.GetParameter(TxParameterID::AtomicSwapTokenContract); + auto symbol = offer.GetParameter(TxParameterID::AtomicSwapTokenSymbol); + if (!contract || !symbol || + !decimals || *decimals > kMaxTokenDecimals || + !IsValidEthContractAddress(*contract) || + !isValidTokenSymbol(*symbol)) + { + return false; + } + } + + auto beamAssetId = offer.GetParameter(TxParameterID::AtomicSwapBeamAssetID); + if (beamAssetId.value_or(0) != 0) + { + if (!offer.GetParameter(TxParameterID::AtomicSwapBeamAssetName)) + { + return false; + } + } + + return true; +} + bool SwapOffersBoard::isOwnOffer(const SwapOffer& offer) const { return m_ownAddresses.find(offer.m_publisherId) != std::cend(m_ownAddresses); @@ -348,6 +430,23 @@ void SwapOffersBoard::sendUpdateToNetwork(const SwapOffer& offer) const void SwapOffersBoard::broadcastOffer(const SwapOffer& offer, uint64_t keyOwnID) const { + // Extended offers (foreign-asset-on-Beam-side or ERC-20 foreign coin) are wire- + // tagged with the ExtendedOffer placeholder so old wallets - which reject + // m_coin >= (old) Unknown - drop them instead of misinterpreting the coin. + // The real coin stays available to new wallets via the AtomicSwapCoin tx param. + static_assert(static_cast(AtomicSwapCoin::ExtendedOffer) >= 10, + "ExtendedOffer must be >= the pre-extension AtomicSwapCoin::Unknown ordinal (10) " + "so old wallets' 'm_coin >= Unknown' guard drops extended offers"); + + if (offer.IsExtended()) + { + SwapOffer wireOffer = offer; + wireOffer.m_coin = AtomicSwapCoin::ExtendedOffer; + auto message = m_protocolHandler.createBroadcastMessage(wireOffer, keyOwnID); + m_broadcastGateway.sendMessage(BroadcastContentType::SwapOffers, message); + return; + } + auto message = m_protocolHandler.createBroadcastMessage(offer, keyOwnID); m_broadcastGateway.sendMessage(BroadcastContentType::SwapOffers, message); } diff --git a/wallet/client/extensions/offers_board/swap_offers_board.h b/wallet/client/extensions/offers_board/swap_offers_board.h index bac211fb7b..d7cc34d76b 100644 --- a/wallet/client/extensions/offers_board/swap_offers_board.h +++ b/wallet/client/extensions/offers_board/swap_offers_board.h @@ -109,6 +109,7 @@ class SwapOffersBoard bool isOfferExpired(const SwapOffer&) const; bool isOfferLifetimeTooLong(const SwapOffer&) const; + bool isExtendedOfferDataValid(const SwapOffer&) const; bool onOfferFromNetwork(SwapOffer& newOffer); void broadcastOffer(const SwapOffer& content, uint64_t keyOwnID) const; void sendUpdateToNetwork(const SwapOffer&) const; diff --git a/wallet/client/wallet_client.cpp b/wallet/client/wallet_client.cpp index 456004e4ea..0733f82227 100644 --- a/wallet/client/wallet_client.cpp +++ b/wallet/client/wallet_client.cpp @@ -1583,6 +1583,13 @@ namespace beam::wallet try { p->publishOffer(offer); + if (m_swapOfferAddress && + offer.GetParameter(TxParameterID::MyAddressID) == m_swapOfferAddress->m_OwnID) + { + // the preview address is now bound to a live offer; + // the next preview gets a fresh one + m_swapOfferAddress.reset(); + } } catch (const std::runtime_error& e) { @@ -1614,6 +1621,19 @@ namespace beam::wallet { res = CreateSwapTransactionParameters(); + // regenerated on every preview edit/new block; reuse one publisher + // address until an offer is actually published with it + if (!m_swapOfferAddress) + { + WalletAddress swapAddr("swap offer"); + m_walletDB->createAddress(swapAddr); + swapAddr.setExpirationStatus(WalletAddress::ExpirationStatus::Auto); + m_walletDB->saveAddress(swapAddr); + m_swapOfferAddress = swapAddr; + } + res.SetParameter(TxParameterID::MyAddressID, m_swapOfferAddress->m_OwnID); + res.SetParameter(TxParameterID::MyAddr, m_swapOfferAddress->m_BbsAddr); + FillSwapTxParams( &res, *m_walletDB, diff --git a/wallet/client/wallet_client.h b/wallet/client/wallet_client.h index 6a63857592..d050c2923c 100644 --- a/wallet/client/wallet_client.h +++ b/wallet/client/wallet_client.h @@ -410,7 +410,12 @@ namespace beam::wallet std::shared_ptr m_thread; const Rules& m_rules; - IWalletDB::Ptr m_walletDB; + IWalletDB::Ptr m_walletDB; +#ifdef BEAM_ATOMIC_SWAP_SUPPORT + // publisher address reused across swap-offer previews; reset once an + // offer is published with it + boost::optional m_swapOfferAddress; +#endif // BEAM_ATOMIC_SWAP_SUPPORT io::Reactor::Ptr m_reactor; IWalletModelAsync::Ptr m_async; std::weak_ptr m_nodeNetwork; diff --git a/wallet/core/common.h b/wallet/core/common.h index 94862dba32..6dc7b3e006 100644 --- a/wallet/core/common.h +++ b/wallet/core/common.h @@ -322,6 +322,14 @@ namespace beam::wallet MACRO(AtomicSwapExternalTx, 37, std::string) \ MACRO(AtomicSwapExternalTxID, 38, std::string) \ MACRO(AtomicSwapExternalTxOutputIndex, 39, uint32_t) \ + /* Extended atomic-swap offers: asset-side and ERC-20 wire data. */ \ + /* Values 41-45 are free (gap between PeerPublicNonce=40 and */ \ + /* PeerPublicExcess=50); frozen by static_assert below the enum. */ \ + MACRO(AtomicSwapBeamAssetID, 41, Asset::ID) \ + MACRO(AtomicSwapBeamAssetName, 42, std::string) \ + MACRO(AtomicSwapTokenContract, 43, std::string) \ + MACRO(AtomicSwapTokenSymbol, 44, std::string) \ + MACRO(AtomicSwapTokenDecimals, 45, uint8_t) \ /* signature parameters */ \ MACRO(PeerPublicNonce, 40, ECC::Point) \ MACRO(PeerPublicExcess, 50, ECC::Point) \ @@ -433,6 +441,13 @@ namespace beam::wallet PeerEndpoint = PeerWalletIdentity, }; + // Wire format stability: these numeric values are serialized and must never change. + static_assert(static_cast(TxParameterID::AtomicSwapBeamAssetID) == 41); + static_assert(static_cast(TxParameterID::AtomicSwapBeamAssetName) == 42); + static_assert(static_cast(TxParameterID::AtomicSwapTokenContract) == 43); + static_assert(static_cast(TxParameterID::AtomicSwapTokenSymbol) == 44); + static_assert(static_cast(TxParameterID::AtomicSwapTokenDecimals) == 45); + using PackedTxParameters = std::vector>; // Holds transaction parameters as key/value diff --git a/wallet/core/strings_resources.cpp b/wallet/core/strings_resources.cpp index af027144d6..c3ec925157 100644 --- a/wallet/core/strings_resources.cpp +++ b/wallet/core/strings_resources.cpp @@ -83,7 +83,6 @@ namespace beam const char kErrorNodePoolPeriodTooMuch[] = "The \"--node_poll_period\" parameter set to more than %1% hours may cause transaction problems."; const char kErrorSwapAmountMissing[] = "swap amount is missing"; const char kErrorSwapCoinUnknown[] = "cannot swap asset coins"; - const char kErrorCantSwapAsset[] = "Unknown coin for swap"; const char kErrorNoBTCNodeCredentials[] = "BTC node credentials should be provided"; const char kErrorSwapAmountTooLow[] = "The swap amount must be greater than the redemption fee."; const char kErrorNoLTCNodeCredentials[] = "LTC node credentials should be provided"; diff --git a/wallet/core/strings_resources.h b/wallet/core/strings_resources.h index eb8154b7a4..36bb60802a 100644 --- a/wallet/core/strings_resources.h +++ b/wallet/core/strings_resources.h @@ -84,7 +84,6 @@ namespace beam extern const char kErrorNodePoolPeriodTooMuch[]; extern const char kErrorSwapAmountMissing[]; extern const char kErrorSwapCoinUnknown[]; - extern const char kErrorCantSwapAsset[]; extern const char kErrorNoBTCNodeCredentials[]; extern const char kErrorSwapAmountTooLow[]; extern const char kErrorNoLTCNodeCredentials[]; diff --git a/wallet/transactions/swaps/CMakeLists.txt b/wallet/transactions/swaps/CMakeLists.txt index d23eedc6e8..0542db7b1d 100644 --- a/wallet/transactions/swaps/CMakeLists.txt +++ b/wallet/transactions/swaps/CMakeLists.txt @@ -35,6 +35,7 @@ set(SWAP_SRC bridges/ethereum/ethereum_base_transaction.cpp bridges/ethereum/ethereum_side.cpp bridges/ethereum/ethereum_bridge.cpp + bridges/ethereum/rpc_endpoint.cpp bridges/ethereum/settings.cpp bridges/ethereum/settings_provider.cpp ) diff --git a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.cpp b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.cpp index b8aff2fdff..9891573127 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.cpp +++ b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.cpp @@ -174,7 +174,7 @@ namespace beam::bitcoin { } - void BitcoinCore016::fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) + void BitcoinCore016::fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) { BEAM_LOG_DEBUG() << "Send fundRawTransaction command"; @@ -187,6 +187,7 @@ namespace beam::bitcoin sendRequest("fundrawtransaction", params, [callback](IBridge::Error error, const json& result) { std::string hex; int changepos = -1; + Amount fee = 0; if (error.m_type == IBridge::None) { @@ -194,6 +195,7 @@ namespace beam::bitcoin { hex = result["hex"].get(); changepos = result["changepos"].get(); + fee = btc_to_satoshi(result["fee"].get()); } catch (const std::exception& ex) { @@ -202,7 +204,7 @@ namespace beam::bitcoin } } - callback(error, hex, changepos); + callback(error, hex, changepos, fee); }); } @@ -591,4 +593,4 @@ namespace beam::bitcoin m_httpClient.send_request(verificationRequest); } } -} // namespace beam::bitcoin \ No newline at end of file +} // namespace beam::bitcoin diff --git a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.h b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.h index eddda0e695..ff5ef57beb 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.h +++ b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.h @@ -26,7 +26,7 @@ namespace beam::bitcoin BitcoinCore016() = delete; BitcoinCore016(io::Reactor& reactor, ISettingsProvider& settingsProvider); - void fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) override; + void fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) override; void signRawTransaction(const std::string& rawTx, std::function callback) override; void sendRawTransaction(const std::string& rawTx, std::function callback) override; void getRawChangeAddress(std::function callback) override; @@ -54,4 +54,4 @@ namespace beam::bitcoin ISettingsProvider& m_settingsProvider; std::map m_verifiedAddresses; }; -} // namespace beam::bitcoin \ No newline at end of file +} // namespace beam::bitcoin diff --git a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.cpp b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.cpp index fa41f862b1..aef1731b4a 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.cpp +++ b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.cpp @@ -540,11 +540,11 @@ namespace beam::wallet return SwapTxState::CreatingTx; } - m_bitcoinBridge->fundRawTransaction(hexTx, GetFeeRate(SubTxIndex::LOCK_TX), [this, weak = this->weak_from_this()](const bitcoin::IBridge::Error& error, const std::string& hexTx, int changePos) + m_bitcoinBridge->fundRawTransaction(hexTx, GetFeeRate(SubTxIndex::LOCK_TX), [this, weak = this->weak_from_this()](const bitcoin::IBridge::Error& error, const std::string& hexTx, int changePos, Amount fee) { if (!weak.expired()) { - OnFundRawTransaction(error, hexTx, changePos); + OnFundRawTransaction(error, hexTx, changePos, fee); } }); return SwapTxState::CreatingTx; @@ -807,7 +807,7 @@ namespace beam::wallet } } - void BitcoinSide::OnFundRawTransaction(const bitcoin::IBridge::Error& error, const std::string& hexTx, int changePos) + void BitcoinSide::OnFundRawTransaction(const bitcoin::IBridge::Error& error, const std::string& hexTx, int changePos, Amount fee) { // TODO: refactor this condition. // Checking !m_SwapLockRawTx.is_initialized() used to ignore double lock on electrum @@ -826,6 +826,20 @@ namespace beam::wallet if (!m_SwapLockRawTx.is_initialized()) { + // The pre-funding estimate assumes a fixed-size tx; verify the actual + // funded fee meets the configured rate before accepting. + // fee == 0 (mock or a bridge that can't report) skips the check. + if (fee > 0 && !bitcoin::IsFundedTxFeeSufficient(hexTx, fee, GetFeeRate(SubTxIndex::LOCK_TX))) + { + BEAM_LOG_ERROR() << m_tx.GetTxID() << "[" << (int)SubTxIndex::LOCK_TX << "]" + << " lock transaction fee rate too low (fee " << fee + << "); consolidate coins or raise the fee rate"; + m_tx.SetParameter(TxParameterID::InternalFailureReason, + TxFailureReason::FeeIsTooSmall, false, SubTxIndex::LOCK_TX); + m_tx.UpdateAsync(); + return; + } + m_SwapLockRawTx = hexTx; m_LockTxValuePosition = changePos ? 0 : 1; m_tx.SetState(SwapTxState::CreatingTx, SubTxIndex::LOCK_TX); @@ -1145,4 +1159,4 @@ namespace beam::wallet m_tx.UpdateAsync(); } } -} \ No newline at end of file +} diff --git a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.h b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.h index 26b2c38869..d39a802b5d 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.h +++ b/wallet/transactions/swaps/bridges/bitcoin/bitcoin_side.h @@ -82,7 +82,7 @@ namespace beam::wallet std::string FillSegwitWithdrawTxInput(SubTxID subTxID); void OnGetRawChangeAddress(const bitcoin::IBridge::Error& error, const std::string& address); - void OnFundRawTransaction(const bitcoin::IBridge::Error& error, const std::string& hexTx, int changePos); + void OnFundRawTransaction(const bitcoin::IBridge::Error& error, const std::string& hexTx, int changePos, Amount fee); void OnSignLockTransaction(const bitcoin::IBridge::Error& error, const std::string& hexTx, bool complete); void OnCreateWithdrawTransaction(SubTxID subTxID, const bitcoin::IBridge::Error& error, const std::string& hexTx); void OnGetSwapLockTxConfirmations(const bitcoin::IBridge::Error& error, const std::string& hexScript, Amount amount, uint32_t confirmations); @@ -102,4 +102,4 @@ namespace beam::wallet boost::optional m_SwapLockRawTx; boost::optional m_SwapWithdrawRawTx; }; -} \ No newline at end of file +} diff --git a/wallet/transactions/swaps/bridges/bitcoin/bridge.h b/wallet/transactions/swaps/bridges/bitcoin/bridge.h index 82b2fc909f..c090a00c81 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/bridge.h +++ b/wallet/transactions/swaps/bridges/bitcoin/bridge.h @@ -45,8 +45,8 @@ namespace beam::bitcoin virtual ~IBridge() {}; - // error, transaction (hex), changepos - virtual void fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) = 0; + // error, transaction (hex), changepos, fee (satoshi, 0 if unknown) + virtual void fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) = 0; //error, transaction (hex), complete virtual void signRawTransaction(const std::string& rawTx, std::function callback) = 0; // error, transaction ID @@ -74,4 +74,4 @@ namespace beam::bitcoin // error, fee rate virtual void estimateFee(int blockAmount, std::function callback) = 0; }; -} // namespace beam::bitcoin \ No newline at end of file +} // namespace beam::bitcoin diff --git a/wallet/transactions/swaps/bridges/bitcoin/common.cpp b/wallet/transactions/swaps/bridges/bitcoin/common.cpp index c6c8949b14..530f327216 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/common.cpp +++ b/wallet/transactions/swaps/bridges/bitcoin/common.cpp @@ -60,4 +60,18 @@ namespace beam::bitcoin return address.encoded(); } + + bool IsFundedTxFeeSufficient(const std::string& hexTx, uint64_t fee, uint64_t feeRate) + { + libbitcoin::data_chunk txData; + if (!libbitcoin::decode_base16(txData, hexTx)) + return true; // undecodable -> let the existing sign/broadcast path report it + + libbitcoin::chain::transaction tx; + if (!tx.from_data(txData, true, true)) + return true; + + uint64_t vsize = tx.serialized_size() + (kMinInputVsize - kUnsignedInputVsize) * tx.inputs().size(); + return fee >= (vsize * feeRate) / 1000u; + } } // namespace beam::bitcoin diff --git a/wallet/transactions/swaps/bridges/bitcoin/common.h b/wallet/transactions/swaps/bridges/bitcoin/common.h index 6633623082..c30e066a5d 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/common.h +++ b/wallet/transactions/swaps/bridges/bitcoin/common.h @@ -30,6 +30,22 @@ namespace beam::bitcoin constexpr uint64_t kDustThreshold = 546; constexpr uint32_t kBTCWithdrawTxAverageSize = 360; constexpr uint32_t kBTCWithdrawSegwitTxAverageSize = 160; + // 107 = legacy P2PKH unlocking script size (without prefix). Used by + // electrum's own calcFee() to estimate the fee for a to-be-funded LOCK_TX, + // where the wallet does not yet know which UTXOs Bitcoin Core will pick. + constexpr size_t kUnlogingScriptSize = 107u; + // 68 = total vsize lower bound of a signed P2WPKH input + // (witness-discounted). Bitcoin Core's fundrawtransaction typically funds + // with segwit inputs, whose vsize is well below the legacy estimate; + // demanding more would make IsFundedTxFeeSufficient reject a + // correctly-funded segwit transaction. Used only by the post-funding + // acceptance gate — it must not replace kUnlogingScriptSize in calcFee's + // pre-funding estimate. + constexpr size_t kMinInputVsize = 68u; + // an unsigned funded input already serializes at 41 bytes + // (36 outpoint + 1 empty-script prefix + 4 sequence); the gate tops each + // input up from that skeleton to kMinInputVsize + constexpr size_t kUnsignedInputVsize = 41u; extern const char kMainnetGenesisBlockHash[]; extern const char kTestnetGenesisBlockHash[]; extern const char kRegtestGenesisBlockHash[]; @@ -38,6 +54,11 @@ namespace beam::bitcoin uint8_t getAddressVersion(); std::vector getGenesisBlockHashes(); + // returns true if fee/vsize meets feeRate for the given funded (post-fundrawtransaction) + // hex-encoded transaction; permissive (returns true) if hexTx cannot be decoded, so an + // undecodable transaction is left to the existing sign/broadcast path to report. + bool IsFundedTxFeeSufficient(const std::string& hexTx, uint64_t fee, uint64_t feeRate); + // the first key is receiving master private key // the second key is changing master private key std::pair generateElectrumMasterPrivateKeys(const std::vector& words); diff --git a/wallet/transactions/swaps/bridges/bitcoin/electrum.cpp b/wallet/transactions/swaps/bridges/bitcoin/electrum.cpp index 64d6d0752a..3f54da5a7c 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/electrum.cpp +++ b/wallet/transactions/swaps/bridges/bitcoin/electrum.cpp @@ -36,7 +36,6 @@ using namespace libbitcoin::chain; namespace { const std::chrono::seconds kRequestPeriod = std::chrono::seconds(10); - const size_t kUnlogingScriptSize = 107u; // without prefix std::string generateScriptHash(const ec_public& publicKey, uint8_t addressVersion) { @@ -52,7 +51,7 @@ namespace { beam::Amount vsize = tx.serialized_size(); - return ((vsize + kUnlogingScriptSize * tx.inputs().size()) * feeRate) / 1000u; + return ((vsize + beam::bitcoin::kUnlogingScriptSize * tx.inputs().size()) * feeRate) / 1000u; } const char kInvalidGenesisBlockHashMsg[] = "Invalid genesis block hash"; @@ -83,7 +82,7 @@ namespace beam::bitcoin } } - void Electrum::fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) + void Electrum::fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) { BEAM_LOG_DEBUG() << "fundRawTransaction command"; @@ -91,7 +90,7 @@ namespace beam::bitcoin { if (error.m_type != ErrorType::None) { - callback(error, "", 0); + callback(error, "", 0, 0); return; } @@ -130,7 +129,7 @@ namespace beam::bitcoin if (resultPoints.value() < total) { IBridge::Error internalError{ ErrorType::BitcoinError, "not enough coins" }; - callback(internalError, "", 0); + callback(internalError, "", 0, 0); return; } @@ -149,6 +148,7 @@ namespace beam::bitcoin auto changeValue = totalInputValue - newTx.total_output_value(); auto fee = calcFee(newTx, feeRate); + Amount effectiveFee = fee; if (fee > changeValue) { @@ -174,6 +174,7 @@ namespace beam::bitcoin { changePosition = static_cast(newTx.outputs().size()) - 1; newTx.outputs().back().set_value(changeValue - newFee); + effectiveFee = newFee; } BEAM_LOG_DEBUG() << "electrum fundrawtransaction: fee = " << newFee << ", size = " << newTx.serialized_size(); @@ -186,7 +187,7 @@ namespace beam::bitcoin BEAM_LOG_DEBUG() << "electrum fundrawtransaction: weight = " << newTx.weight() << ", fee = " << fee << ", size = " << newTx.serialized_size(); - callback(error, encode_base16(newTx.to_data()), changePosition); + callback(error, encode_base16(newTx.to_data()), changePosition, effectiveFee); return; } } @@ -195,7 +196,7 @@ namespace beam::bitcoin Error tmp; tmp.m_type = IBridge::BitcoinError; tmp.m_message = err.what(); - callback(tmp, "", -1); + callback(tmp, "", -1, 0); } }); } @@ -989,4 +990,4 @@ namespace beam::bitcoin return signTx; } -} // namespace beam::bitcoin \ No newline at end of file +} // namespace beam::bitcoin diff --git a/wallet/transactions/swaps/bridges/bitcoin/electrum.h b/wallet/transactions/swaps/bridges/bitcoin/electrum.h index d9a1cf4bac..2f487406c3 100644 --- a/wallet/transactions/swaps/bridges/bitcoin/electrum.h +++ b/wallet/transactions/swaps/bridges/bitcoin/electrum.h @@ -60,7 +60,7 @@ namespace beam::bitcoin Electrum(beam::io::Reactor& reactor, ISettingsProvider& settingsProvider); ~Electrum() override; - void fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) override; + void fundRawTransaction(const std::string& rawTx, Amount feeRate, std::function callback) override; void signRawTransaction(const std::string& rawTx, std::function callback) override; void sendRawTransaction(const std::string& rawTx, std::function callback) override; void getRawChangeAddress(std::function callback) override; @@ -118,4 +118,4 @@ namespace beam::bitcoin io::AsyncEvent::Ptr m_asyncEvent; std::map m_verifiedAddresses; }; -} // namespace beam::bitcoin \ No newline at end of file +} // namespace beam::bitcoin diff --git a/wallet/transactions/swaps/bridges/ethereum/bridge.h b/wallet/transactions/swaps/bridges/ethereum/bridge.h index e632fd8647..89db497667 100644 --- a/wallet/transactions/swaps/bridges/ethereum/bridge.h +++ b/wallet/transactions/swaps/bridges/ethereum/bridge.h @@ -34,7 +34,8 @@ class IBridge InvalidResultFormat, IOError, EthError, - EmptyResult + EmptyResult, + InvalidNetwork }; struct Error @@ -49,9 +50,15 @@ class IBridge virtual void getBalance(std::function callback) = 0; virtual void getTokenBalance( - const std::string& contractAddr, + const std::string& contractAddr, std::function callback) = 0; + // Queries symbol() and decimals() of an arbitrary ERC-20 contract (used for + // per-offer tokens, where the wallet has no static entry for the contract). + virtual void getTokenInfo( + const std::string& contractAddr, + std::function callback) = 0; virtual void getBlockNumber(std::function callback) = 0; + virtual void getChainID(std::function callback) = 0; virtual void getTransactionCount(std::function callback) = 0; virtual void sendRawTransaction(const std::string& rawTx, std::function callback) = 0; virtual void send( @@ -75,4 +82,4 @@ class IBridge virtual libbitcoin::short_hash generateEthAddress() const = 0; virtual void getGasPrice(std::function callback) = 0; }; -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/client.cpp b/wallet/transactions/swaps/bridges/ethereum/client.cpp index da250f6697..ceaf9f858f 100644 --- a/wallet/transactions/swaps/bridges/ethereum/client.cpp +++ b/wallet/transactions/swaps/bridges/ethereum/client.cpp @@ -56,11 +56,26 @@ struct EthereumClientBridge : public Bridge call_async(&IClientAsync::GetBalance, swapCoin); } + void GetTokenBalance(const std::string& tokenContract, uint8_t decimals) + { + call_async(&IClientAsync::GetTokenBalance, tokenContract, decimals); + } + + void GetTokenInfo(const std::string& tokenContract) + { + call_async(&IClientAsync::GetTokenInfo, tokenContract); + } + void EstimateGasPrice() { call_async(&IClientAsync::EstimateGasPrice); } + void ValidateEndpoint() + { + call_async(&IClientAsync::ValidateEndpoint); + } + void ChangeSettings(const Settings& settings) { call_async(&IClientAsync::ChangeSettings, settings); @@ -147,6 +162,57 @@ void Client::GetBalance(wallet::AtomicSwapCoin swapCoin) } } +void Client::GetTokenBalance(const std::string& tokenContract, uint8_t decimals) +{ + auto bridge = GetBridge(); + + if (!bridge) + { + return; + } + + bridge->getTokenBalance(tokenContract, [this, weak = this->weak_from_this(), tokenContract, decimals](const IBridge::Error& error, const std::string& balance) + { + if (weak.expired()) + { + return; + } + + SetConnectionError(error.m_type); + SetStatus((error.m_type != IBridge::None) ? Status::Failed : Status::Connected); + + if (error.m_type == IBridge::None) + { + boost::multiprecision::uint256_t tmp(balance); + tmp /= ethereum::TokenUnitsMultiplier(decimals); + + OnTokenBalance(tokenContract, tmp.convert_to()); + } + }); +} + +void Client::GetTokenInfo(const std::string& tokenContract) +{ + auto bridge = GetBridge(); + + if (!bridge) + { + // empty message: callers show their own no-connection text + OnTokenInfo(tokenContract, {}, 0, { IBridge::IOError, {} }); + return; + } + + bridge->getTokenInfo(tokenContract, [this, weak = this->weak_from_this(), tokenContract](const IBridge::Error& error, const std::string& symbol, uint8_t decimals) + { + if (weak.expired()) + { + return; + } + + OnTokenInfo(tokenContract, symbol, decimals, error); + }); +} + void Client::EstimateGasPrice() { auto bridge = GetBridge(); @@ -175,7 +241,7 @@ void Client::EstimateGasPrice() if (GetSettings().GetMinFeeRate() > result) { - result = 0; + result = GetSettings().GetMinFeeRate(); } OnEstimatedGasPrice(result); @@ -183,6 +249,54 @@ void Client::EstimateGasPrice() }); } +void Client::ValidateEndpoint() +{ + auto bridge = GetBridge(); + + if (!bridge) + { + return; + } + + bridge->getChainID([this, weak = this->weak_from_this(), bridge](const IBridge::Error& error, uint64_t chainID) + { + if (weak.expired()) + { + return; + } + + if (error.m_type != IBridge::None) + { + SetConnectionError(error.m_type); + SetStatus(Status::Failed); + OnEndpointValidated(0, 0, error); + return; + } + + bridge->getBlockNumber([this, weak, chainID](const IBridge::Error& error, uint64_t blockNumber) + { + if (weak.expired()) + { + return; + } + + IBridge::Error finalError = error; + // Mainnet builds require chain id 1; testnet builds accept any + // chain (private nets / forks are the use case). + if (finalError.m_type == IBridge::None && + wallet::UseMainnetSwap() && chainID != 1) + { + finalError.m_type = IBridge::InvalidNetwork; + finalError.m_message = "endpoint is not an Ethereum mainnet node"; + } + + SetConnectionError(finalError.m_type); + SetStatus((finalError.m_type != IBridge::None) ? Status::Failed : Status::Connected); + OnEndpointValidated(chainID, blockNumber, finalError); + }); + }); +} + void Client::ChangeSettings(const Settings& settings) { { @@ -267,4 +381,4 @@ void Client::SetConnectionError(const IBridge::ErrorType& error) OnConnectionError(m_connectionError); } } -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/client.h b/wallet/transactions/swaps/bridges/ethereum/client.h index fb43d978b4..e5943760b7 100644 --- a/wallet/transactions/swaps/bridges/ethereum/client.h +++ b/wallet/transactions/swaps/bridges/ethereum/client.h @@ -18,7 +18,7 @@ #include "bridge_holder.h" namespace beam::ethereum -{ +{ class IClientAsync { public: @@ -26,9 +26,12 @@ class IClientAsync virtual void GetStatus() = 0; virtual void GetBalance(wallet::AtomicSwapCoin swapCoin) = 0; + virtual void GetTokenBalance(const std::string& tokenContract, uint8_t decimals) = 0; + virtual void GetTokenInfo(const std::string& tokenContract) = 0; virtual void EstimateGasPrice() = 0; + virtual void ValidateEndpoint() = 0; virtual void ChangeSettings(const Settings& settings) = 0; -}; +}; class Client : private IClientAsync @@ -58,10 +61,15 @@ class Client virtual void OnStatus(Status status) = 0; // balance in gwei virtual void OnBalance(wallet::AtomicSwapCoin swapCoin, Amount balance) = 0; + // balance in wallet units (decimals already normalized), keyed by contract address + virtual void OnTokenBalance(const std::string& tokenContract, Amount balance) {} + // symbol()/decimals() as reported by the contract + virtual void OnTokenInfo(const std::string& tokenContract, const std::string& symbol, uint8_t decimals, const IBridge::Error& error) {} virtual void OnEstimatedGasPrice(Amount gasPrice) = 0; virtual void OnCanModifySettingsChanged(bool canModify) = 0; virtual void OnChangedSettings() = 0; virtual void OnConnectionError(IBridge::ErrorType error) = 0; + virtual void OnEndpointValidated(uint64_t chainID, uint64_t blockNumber, const IBridge::Error& error) {} bool CanModify() const override; void AddRef() override; @@ -71,7 +79,10 @@ class Client // IClientAsync void GetStatus() override; void GetBalance(wallet::AtomicSwapCoin swapCoin) override; + void GetTokenBalance(const std::string& tokenContract, uint8_t decimals) override; + void GetTokenInfo(const std::string& tokenContract) override; void EstimateGasPrice() override; + void ValidateEndpoint() override; void ChangeSettings(const Settings& settings) override; void SetStatus(const Status& status); @@ -91,4 +102,4 @@ class Client size_t m_refCount = 0; IBridge::ErrorType m_connectionError = IBridge::ErrorType::None; }; -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/common.cpp b/wallet/transactions/swaps/bridges/ethereum/common.cpp index 1fba2f5cf4..0d2954d928 100644 --- a/wallet/transactions/swaps/bridges/ethereum/common.cpp +++ b/wallet/transactions/swaps/bridges/ethereum/common.cpp @@ -13,6 +13,7 @@ // limitations under the License. #include "common.h" +#include #include #include #include @@ -109,26 +110,54 @@ ECC::uintBig ConvertStrToUintBig(const std::string& number, bool hex) ECC::uintBig result = ECC::Zero; std::copy(dc.crbegin(), dc.crend(), std::rbegin(result.m_pData)); return result; -} - -std::string AddHexPrefix(const std::string& value) -{ - if (!HasHexPrefix(value)) - { - return kHexPrefix + value; - } - - return value; -} - -std::string RemoveHexPrefix(const std::string& value) -{ - if (HasHexPrefix(value)) - { - return std::string(value.begin() + 2, value.end()); - } - - return value; +} + +std::string AddHexPrefix(const std::string& value) +{ + if (!HasHexPrefix(value)) + { + return kHexPrefix + value; + } + + return value; +} + +std::string RemoveHexPrefix(const std::string& value) +{ + if (HasHexPrefix(value)) + { + return std::string(value.begin() + 2, value.end()); + } + + return value; +} + +bool ParseTokenDecimalsWord(const std::string& hexWord, uint8_t& decimals) +{ + if (hexWord.size() != kEthContractABIWordSize * 2) + { + return false; + } + + if (hexWord.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) + { + return false; + } + + std::string lowByteHex = hexWord.substr(hexWord.size() - 2); + std::string highBytesHex = hexWord.substr(0, hexWord.size() - 2); + if (highBytesHex.find_first_not_of('0') != std::string::npos) + { + return false; + } + + decimals = static_cast(std::stoul(lowByteHex, nullptr, 16)); + if (decimals > wallet::kMaxTokenDecimals) + { + return false; + } + + return true; } void AddContractABIWordToBuffer(const libbitcoin::data_slice& src, libbitcoin::data_chunk& dst) @@ -140,61 +169,99 @@ void AddContractABIWordToBuffer(const libbitcoin::data_slice& src, libbitcoin::d } dst.insert(dst.end(), src.begin(), src.end()); } - -uint32_t GetCoinUnitsMultiplier(beam::wallet::AtomicSwapCoin swapCoin) -{ - switch (swapCoin) - { - case beam::wallet::AtomicSwapCoin::Ethereum: - case beam::wallet::AtomicSwapCoin::Dai: - return 1'000'000'000u; - case beam::wallet::AtomicSwapCoin::Usdt: - case beam::wallet::AtomicSwapCoin::WBTC: - return 1u; - default: - assert(false && "Unexpected swapCoin!"); - return 1u; - } + +uint32_t GetCoinUnitsMultiplier(beam::wallet::AtomicSwapCoin swapCoin) +{ + switch (swapCoin) + { + case beam::wallet::AtomicSwapCoin::Ethereum: + case beam::wallet::AtomicSwapCoin::Dai: + return 1'000'000'000u; + case beam::wallet::AtomicSwapCoin::Usdt: + case beam::wallet::AtomicSwapCoin::WBTC: + return 1u; + default: + assert(false && "Unexpected swapCoin!"); + return 1u; + } +} + +uint64_t WalletUnitsPerToken(uint8_t decimals) +{ + // Primary guard is upstream (isExtendedOfferDataValid / getTokenInfo); this + // is only a backstop against a programmer error letting an out-of-range + // decimals slip through. + assert(decimals <= wallet::kMaxTokenDecimals); + if (decimals > wallet::kMaxTokenDecimals) + { + decimals = wallet::kMaxTokenDecimals; + } + + uint8_t walletDecimals = std::min(decimals, 9); + uint64_t result = 1; + for (uint8_t i = 0; i < walletDecimals; ++i) + { + result *= 10; + } + return result; +} + +uint32_t TokenUnitsMultiplier(uint8_t decimals) +{ + // See WalletUnitsPerToken: belt-and-braces clamp, not the primary guard. + assert(decimals <= wallet::kMaxTokenDecimals); + if (decimals > wallet::kMaxTokenDecimals) + { + decimals = wallet::kMaxTokenDecimals; + } + + uint8_t extraDecimals = (decimals > 9) ? (decimals - 9) : 0; + uint32_t result = 1; + for (uint8_t i = 0; i < extraDecimals; ++i) + { + result *= 10; + } + return result; } bool IsEthereumBased(wallet::AtomicSwapCoin swapCoin) { - switch (swapCoin) - { - case beam::wallet::AtomicSwapCoin::Ethereum: - case beam::wallet::AtomicSwapCoin::Dai: - case beam::wallet::AtomicSwapCoin::Usdt: - case beam::wallet::AtomicSwapCoin::WBTC: - return true; - default: - return false; + switch (swapCoin) + { + case beam::wallet::AtomicSwapCoin::Ethereum: + case beam::wallet::AtomicSwapCoin::Dai: + case beam::wallet::AtomicSwapCoin::Usdt: + case beam::wallet::AtomicSwapCoin::WBTC: + return true; + default: + return false; } } namespace swap_contract { - std::string GetRefundMethodHash(bool isHashLockScheme) - { - return isHashLockScheme ? "7249fbb6" : "fa89401a"; - } - - std::string GetLockMethodHash(bool isErc20, bool isHashLockScheme) - { - if (isErc20) - { - return isHashLockScheme ? "15601f4f" : "71c472e6"; - } - return isHashLockScheme ? "ae052147" : "bc18cc34"; - } - - std::string GetRedeemMethodHash(bool isHashLockScheme) - { - return isHashLockScheme ? "b31597ad" : "8772acd6"; - } - - std::string GetDetailsMethodHash(bool isHashLockScheme) - { - return isHashLockScheme ? "6bfec360" : "7cf3285f"; + std::string GetRefundMethodHash(bool isHashLockScheme) + { + return isHashLockScheme ? "7249fbb6" : "fa89401a"; + } + + std::string GetLockMethodHash(bool isErc20, bool isHashLockScheme) + { + if (isErc20) + { + return isHashLockScheme ? "15601f4f" : "71c472e6"; + } + return isHashLockScheme ? "ae052147" : "bc18cc34"; + } + + std::string GetRedeemMethodHash(bool isHashLockScheme) + { + return isHashLockScheme ? "b31597ad" : "8772acd6"; + } + + std::string GetDetailsMethodHash(bool isHashLockScheme) + { + return isHashLockScheme ? "6bfec360" : "7cf3285f"; } } // namespace swap_contract -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/common.h b/wallet/transactions/swaps/bridges/ethereum/common.h index e2d8f3a9c6..56f5942ac2 100644 --- a/wallet/transactions/swaps/bridges/ethereum/common.h +++ b/wallet/transactions/swaps/bridges/ethereum/common.h @@ -44,6 +44,27 @@ void AddContractABIWordToBuffer(const libbitcoin::data_slice& src, libbitcoin::d uint32_t GetCoinUnitsMultiplier(beam::wallet::AtomicSwapCoin swapCoin); bool IsEthereumBased(wallet::AtomicSwapCoin swapCoin); +// Parses an ABI uint256 word (64 hex chars, no 0x) as token decimals. +// Fails when any byte above the lowest is set or the value exceeds +// kMaxTokenDecimals. +bool ParseTokenDecimalsWord(const std::string& hexWord, uint8_t& decimals); + +// Per-offer ERC-20 token (AtomicSwapCoin::Erc20Token) equivalents of +// UnitsPerCoin/GetCoinUnitsMultiplier, parameterized by the token's on-chain +// decimals (TxParameterID::AtomicSwapTokenDecimals) instead of a fixed table. +// walletDecimals = min(decimals, 9); on-wire value = Amount * TokenUnitsMultiplier(decimals). +// These reproduce today's constants for classic coins: ETH/DAI (18 -> 10^9/10^9), +// USDT (6 -> 10^6/1), WBTC (8 -> 10^8/1). +// decimals is attacker-controlled (comes from the counterparty's contract via +// getTokenInfo, or from a peer's offer-board TxParameterID::AtomicSwapTokenDecimals), +// so it must be bounded by kMaxTokenDecimals before it reaches these helpers. +// isExtendedOfferDataValid() and getTokenInfo() are the primary guards; the +// assert+clamp below is only a backstop against a programmer error letting an +// out-of-range decimals slip through, never the primary defense (TokenUnitsMultiplier +// computes 10^(decimals-9) in a uint32_t, which wraps/zeroes for decimals >= 19). +uint64_t WalletUnitsPerToken(uint8_t decimals); // = 10^min(decimals, 9) +uint32_t TokenUnitsMultiplier(uint8_t decimals); // = 10^max(0, decimals - 9) + namespace ERC20Hashes { // "allowance(address,address)" @@ -62,6 +83,8 @@ namespace ERC20Hashes inline const char* kNameHash = "06fdde03"; // "decimals()" inline const char* kDecimalsHash = "313ce567"; + // "symbol()" + inline const char* kSymbolHash = "95d89b41"; } // namespace ERC20Hashes namespace swap_contract @@ -71,4 +94,4 @@ namespace swap_contract std::string GetRedeemMethodHash(bool isHashLockScheme); std::string GetDetailsMethodHash(bool isHashLockScheme); } // swap_contract -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.cpp b/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.cpp index 3feed92e73..3511921a7a 100644 --- a/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.cpp +++ b/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.cpp @@ -15,6 +15,7 @@ #include "ethereum_bridge.h" #include "common.h" +#include "rpc_endpoint.h" #include "utility/logger.h" #include "nlohmann/json.hpp" @@ -30,11 +31,86 @@ using json = nlohmann::json; namespace { -bool needSsl(const std::string& address) -{ - // TODO roman.strilets need insensitive - return address.find("infura") != std::string::npos; -} + // Folds the low 8 bytes of a 32-byte big-endian ABI word into a uint64_t. + // Sufficient for ABI offsets/lengths, which are always tiny for a symbol() string. + uint64_t LowUint64OfAbiWord(libbitcoin::data_chunk::const_iterator wordBegin) + { + uint64_t value = 0; + auto begin = wordBegin + (beam::ethereum::kEthContractABIWordSize - 8); + for (auto it = begin; it != wordBegin + beam::ethereum::kEthContractABIWordSize; ++it) + { + value = (value << 8) | *it; + } + return value; + } + + // Decodes an ERC-20 symbol() eth_call result. Standard tokens return an + // ABI-encoded dynamic string ([offset][length][data]); some non-standard + // tokens (e.g. MKR) return a raw bytes32 value instead. Falls back to the + // trimmed raw bytes when the standard layout does not parse. + std::string DecodeAbiStringOrRawBytes(const std::string& hexResult) + { + auto raw = beam::ethereum::RemoveHexPrefix(hexResult); + libbitcoin::data_chunk bytes; + if (!raw.empty()) + { + libbitcoin::decode_base16(bytes, raw); + } + + constexpr size_t kWord = beam::ethereum::kEthContractABIWordSize; + + if (bytes.size() >= 2 * kWord) + { + uint64_t offset = LowUint64OfAbiWord(bytes.begin()); + if (offset <= bytes.size() && bytes.size() - offset >= kWord) + { + auto lengthBegin = bytes.begin() + offset; + uint64_t length = LowUint64OfAbiWord(lengthBegin); + auto dataBegin = lengthBegin + kWord; + if (length <= static_cast(bytes.end() - dataBegin)) + { + return std::string(dataBegin, dataBegin + length); + } + } + } + + // fallback: raw fixed-size (bytes32-like) value, trim trailing zero padding + auto end = bytes.end(); + while (end != bytes.begin() && *(end - 1) == 0) + { + --end; + } + return std::string(bytes.begin(), end); + } + + // A token's symbol() result is attacker-controlled (the contract belongs to + // the counterparty) and reaches the CLI, API and UI verbatim. Strip + // non-printable-ASCII bytes and cap the length so it can't be used to smuggle + // control characters or oversized strings into those surfaces; fall back to a + // generic placeholder if nothing sensible remains. + constexpr size_t kMaxSymbolLength = 32; + + std::string SanitizeTokenSymbol(const std::string& symbol) + { + std::string sanitized; + sanitized.reserve(std::min(symbol.size(), kMaxSymbolLength)); + for (unsigned char c : symbol) + { + if (c >= 0x20 && c <= 0x7E) + { + sanitized.push_back(static_cast(c)); + if (sanitized.size() >= kMaxSymbolLength) + { + break; + } + } + } + if (sanitized.empty()) + { + sanitized = "ERC20"; + } + return sanitized; + } } namespace beam::ethereum @@ -108,6 +184,86 @@ void EthereumBridge::getTokenBalance( }); } +void EthereumBridge::getTokenInfo( + const std::string& contractAddr, + std::function callback) +{ + BEAM_LOG_DEBUG() << "EthereumBridge::getTokenInfo"; + const auto tokenContractAddress = ethereum::ConvertStrToEthAddress(contractAddr); + + libbitcoin::data_chunk decimalsData; + decimalsData.reserve(ethereum::kEthContractMethodHashSize); + libbitcoin::decode_base16(decimalsData, ethereum::ERC20Hashes::kDecimalsHash); + + call(tokenContractAddress, libbitcoin::encode_base16(decimalsData), + [this, tokenContractAddress, callback](const IBridge::Error& decError, const nlohmann::json& decResult) + { + BEAM_LOG_DEBUG() << "EthereumBridge::getTokenInfo (decimals) in"; + Error error = decError; + uint8_t decimals = 0; + + if (error.m_type == IBridge::None) + { + try + { + auto raw = ethereum::RemoveHexPrefix(decResult.get()); + if (raw.empty()) + { + throw std::runtime_error("empty decimals() result"); + } + // decimals() returns a uint256; a legitimate token's decimals fits in + // the low byte. decimals is attacker-controlled (this contract belongs + // to the counterparty) and unconditionally feeds wire-amount arithmetic + // (WalletUnitsPerToken/TokenUnitsMultiplier), where an out-of-range value + // wraps/zeroes silently. Reject rather than truncate: any nonzero byte + // above the low byte, or a low byte beyond kMaxTokenDecimals, is invalid. + if (!ethereum::ParseTokenDecimalsWord(raw, decimals)) + { + throw std::runtime_error("decimals() value out of range"); + } + } + catch (const std::exception& ex) + { + error.m_type = IBridge::InvalidResultFormat; + error.m_message = ex.what(); + } + } + + if (error.m_type != IBridge::None) + { + callback(error, "", 0); + return; + } + + libbitcoin::data_chunk symbolData; + symbolData.reserve(ethereum::kEthContractMethodHashSize); + libbitcoin::decode_base16(symbolData, ethereum::ERC20Hashes::kSymbolHash); + + call(tokenContractAddress, libbitcoin::encode_base16(symbolData), + [decimals, callback](const IBridge::Error& symError, const nlohmann::json& symResult) + { + BEAM_LOG_DEBUG() << "EthereumBridge::getTokenInfo (symbol) in"; + Error error = symError; + std::string symbol; + + if (error.m_type == IBridge::None) + { + try + { + symbol = DecodeAbiStringOrRawBytes(symResult.get()); + } + catch (const std::exception& ex) + { + error.m_type = IBridge::InvalidResultFormat; + error.m_message = ex.what(); + } + } + + callback(error, error.m_type == IBridge::None ? SanitizeTokenSymbol(symbol) : symbol, decimals); + }); + }); +} + void EthereumBridge::getBlockNumber(std::function callback) { BEAM_LOG_DEBUG() << "EthereumBridge::getBlockNumber"; @@ -133,6 +289,31 @@ void EthereumBridge::getBlockNumber(std::function }); } +void EthereumBridge::getChainID(std::function callback) +{ + BEAM_LOG_DEBUG() << "EthereumBridge::getChainID"; + sendRequest("eth_chainId", "", [callback](Error error, const json& result) + { + BEAM_LOG_DEBUG() << "EthereumBridge::getChainID in"; + uint64_t chainID = 0; + + if (error.m_type == IBridge::None) + { + try + { + std::string strChainID = result["result"].get(); + chainID = std::stoull(strChainID, nullptr, 16); + } + catch (const std::exception& ex) + { + error.m_type = IBridge::InvalidResultFormat; + error.m_message = ex.what(); + } + } + callback(error, chainID); + }); +} + void EthereumBridge::getTransactionCount(std::function callback) { BEAM_LOG_DEBUG() << "EthereumBridge::getTransactionCount"; @@ -698,10 +879,11 @@ void EthereumBridge::sendRequest( if (!address.resolve(url.c_str())) { - BEAM_LOG_ERROR() << "unable to resolve electrum address: " << url; + // url is host:port from GetEthNodeAddress(), no path/query key material + BEAM_LOG_ERROR() << "unable to resolve ethereum provider address: " << url; // TODO maybe to need async?? - Error error{ IOError, "unable to resolve ethereum provider address: " + url }; + Error error{ IOError, "unable to resolve ethereum provider address" }; json result; callback(error, result); return; @@ -785,4 +967,4 @@ libbitcoin::ec_secret EthereumBridge::generatePrivateKey() const return GeneratePrivateKey(settings.m_secretWords, settings.m_accountIndex); } -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.h b/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.h index 9ddcca3774..ee797d7117 100644 --- a/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.h +++ b/wallet/transactions/swaps/bridges/ethereum/ethereum_bridge.h @@ -33,7 +33,11 @@ class EthereumBridge : public IBridge, public std::enable_shared_from_this callback) override; + void getTokenInfo( + const std::string& contractAddr, + std::function callback) override; void getBlockNumber(std::function callback) override; + void getChainID(std::function callback) override; void getTransactionCount(std::function callback) override; void sendRawTransaction(const std::string& rawTx, std::function callback) override; void send( @@ -120,4 +124,4 @@ class EthereumBridge : public IBridge, public std::enable_shared_from_this> m_pendingApprovals; }; -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/ethereum_side.cpp b/wallet/transactions/swaps/bridges/ethereum/ethereum_side.cpp index a5174979ec..4ebf5b25fa 100644 --- a/wallet/transactions/swaps/bridges/ethereum/ethereum_side.cpp +++ b/wallet/transactions/swaps/bridges/ethereum/ethereum_side.cpp @@ -355,8 +355,7 @@ bool EthereumSide::SendLockTx() ethereum::AddContractABIWordToBuffer(GetContractAddress(), data); ethereum::AddContractABIWordToBuffer({ std::begin(swapAmount.m_pData), std::end(swapAmount.m_pData) }, data); - auto swapCoin = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapCoin); - const auto tokenContractAddress = ethereum::ConvertStrToEthAddress(m_settingsProvider.GetSettings().GetTokenContractAddress(swapCoin)); + const auto tokenContractAddress = GetTokenContractAddress(); m_ethBridge->erc20Approve(tokenContractAddress, GetContractAddress(), swapAmount, GetApproveTxGasLimit(), GetGasPrice(SubTxIndex::LOCK_TX), [this, weak = this->weak_from_this()](const ethereum::IBridge::Error& error, std::string txHash) @@ -504,8 +503,7 @@ beam::ByteBuffer EthereumSide::BuildRedeemTxData() if (IsERC20Token()) { // add TokenContractAddress - auto swapCoin = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapCoin); - const auto tokenContractAddress = ethereum::ConvertStrToEthAddress(m_settingsProvider.GetSettings().GetTokenContractAddress(swapCoin)); + const auto tokenContractAddress = GetTokenContractAddress(); hashData.insert(hashData.end(), tokenContractAddress.cbegin(), tokenContractAddress.cend()); } @@ -556,6 +554,7 @@ bool EthereumSide::IsERC20Token() const case beam::wallet::AtomicSwapCoin::Dai: case beam::wallet::AtomicSwapCoin::Usdt: case beam::wallet::AtomicSwapCoin::WBTC: + case beam::wallet::AtomicSwapCoin::Erc20Token: return true; case beam::wallet::AtomicSwapCoin::Ethereum: return false; @@ -567,6 +566,17 @@ bool EthereumSide::IsERC20Token() const } } +libbitcoin::short_hash EthereumSide::GetTokenContractAddress() const +{ + auto swapCoin = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapCoin); + if (swapCoin == AtomicSwapCoin::Erc20Token) + { + auto contractAddrStr = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapTokenContract); + return ethereum::ConvertStrToEthAddress(contractAddrStr); + } + return ethereum::ConvertStrToEthAddress(m_settingsProvider.GetSettings().GetTokenContractAddress(swapCoin)); +} + beam::ByteBuffer EthereumSide::BuildLockTxData() { auto participantStr = m_isEthOwner ? @@ -586,8 +596,7 @@ beam::ByteBuffer EthereumSide::BuildLockTxData() if (IsERC20Token()) { // + ERC20 contractAddress, + value - auto swapCoin = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapCoin); - const auto tokenContractAddress = ethereum::ConvertStrToEthAddress(m_settingsProvider.GetSettings().GetTokenContractAddress(swapCoin)); + const auto tokenContractAddress = GetTokenContractAddress(); uintBig swapAmount = GetSwapAmount(); ethereum::AddContractABIWordToBuffer(tokenContractAddress, out); @@ -634,7 +643,11 @@ ECC::uintBig EthereumSide::GetSwapAmount() const auto swapCoin = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapCoin); uintBig swapAmount = m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapAmount); - auto num = swapAmount.ToNumber() * MultiWord::From(ethereum::GetCoinUnitsMultiplier(swapCoin)); + uint32_t unitsMultiplier = (swapCoin == AtomicSwapCoin::Erc20Token) + ? ethereum::TokenUnitsMultiplier(m_tx.GetMandatoryParameter(TxParameterID::AtomicSwapTokenDecimals)) + : ethereum::GetCoinUnitsMultiplier(swapCoin); + + auto num = swapAmount.ToNumber() * MultiWord::From(unitsMultiplier); swapAmount.FromNumber(num); return swapAmount; diff --git a/wallet/transactions/swaps/bridges/ethereum/ethereum_side.h b/wallet/transactions/swaps/bridges/ethereum/ethereum_side.h index 28df1b744a..727599961e 100644 --- a/wallet/transactions/swaps/bridges/ethereum/ethereum_side.h +++ b/wallet/transactions/swaps/bridges/ethereum/ethereum_side.h @@ -74,9 +74,10 @@ namespace beam::wallet beam::ByteBuffer BuildRedeemTxData(); beam::ByteBuffer BuildRefundTxData(); - bool IsERC20Token() const; + bool IsERC20Token() const; beam::ByteBuffer BuildLockTxData(); ECC::uintBig GetSwapAmount() const; + libbitcoin::short_hash GetTokenContractAddress() const; bool IsHashLockScheme() const; void SetTxError(const ethereum::IBridge::Error& error, SubTxID subTxID); @@ -93,4 +94,4 @@ namespace beam::wallet uint32_t m_WithdrawTxConfirmations = 0; uint64_t m_WithdrawTxBlockNumber = 0; }; -} // namespace beam::wallet \ No newline at end of file +} // namespace beam::wallet diff --git a/wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.cpp b/wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.cpp new file mode 100644 index 0000000000..365d9a2525 --- /dev/null +++ b/wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.cpp @@ -0,0 +1,136 @@ +// Copyright 2020 The Beam Team +// +// 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. + +#include "rpc_endpoint.h" +#include +#include + +namespace beam::ethereum +{ +namespace +{ +std::string trimOuter(const std::string& s) +{ + auto b = s.find_first_not_of(" \t\r\n"); + if (b == std::string::npos) return {}; + auto e = s.find_last_not_of(" \t\r\n"); + return s.substr(b, e - b + 1); +} + +bool hasForbiddenChars(const std::string& s) +{ + return std::any_of(s.begin(), s.end(), [](unsigned char c) { + return c <= 0x20 || c == 0x7f; // whitespace + control chars + }); +} + +std::string toLower(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + return s; +} + +bool validHostChar(char c) +{ + return std::isalnum((unsigned char)c) || c == '.' || c == '-'; +} + +} // namespace + +bool ParseEthereumRpcUrl(const std::string& rawUrl, RpcEndpoint& out) +{ + const std::string url = trimOuter(rawUrl); + if (url.empty() || hasForbiddenChars(url)) + return false; + + // scheme + auto schemeEnd = url.find("://"); + if (schemeEnd == std::string::npos) + return false; + const std::string scheme = toLower(url.substr(0, schemeEnd)); + bool ssl; + if (scheme == "https") ssl = true; + else if (scheme == "http") ssl = false; + else return false; + + std::string rest = url.substr(schemeEnd + 3); + + // split authority / path + std::string::size_type pathPos; + if (!rest.empty() && rest.front() == '[') + { + auto close = rest.find(']'); + auto slash = rest.find('/'); + if (close == std::string::npos || (slash != std::string::npos && slash < close)) + return false; // unterminated bracket, or '/' before closing bracket + pathPos = rest.find('/', close); + } + else + { + pathPos = rest.find('/'); + } + std::string authority = (pathPos == std::string::npos) ? rest : rest.substr(0, pathPos); + std::string pathAndQuery = (pathPos == std::string::npos) ? "/" : rest.substr(pathPos); + + if (authority.empty() || authority.find('@') != std::string::npos) // no credentials + return false; + + // host[:port]. Bracketed IPv6 literals are rejected: the HTTP transport + // (io::Address::resolve) is IPv4-only and splits host:port on the first + // colon, so an accepted IPv6 endpoint could never connect. + std::string host; + std::string portStr; + if (authority.front() == '[') + return false; + + auto colon = authority.find(':'); + host = authority.substr(0, colon == std::string::npos ? authority.size() : colon); + if (colon != std::string::npos) + portStr = authority.substr(colon + 1); + if (host.empty() || + !std::all_of(host.begin(), host.end(), validHostChar)) + return false; + + uint16_t port = ssl ? 443 : 80; + if (!portStr.empty()) + { + if (portStr.size() > 5 || + !std::all_of(portStr.begin(), portStr.end(), + [](unsigned char c) { return std::isdigit(c); })) + return false; + unsigned long p = std::stoul(portStr); + if (p < 1 || p > 65535) + return false; + port = static_cast(p); + } + + out.m_ssl = ssl; + out.m_host = toLower(host); + out.m_port = port; + out.m_pathAndQuery = pathAndQuery; // case preserved: API keys are case-sensitive + return true; +} + +std::string SanitizeRpcUrlForLog(const std::string& url) +{ + RpcEndpoint ep; + if (!ParseEthereumRpcUrl(url, ep)) + return ""; + std::string res = (ep.m_ssl ? "https://" : "http://") + ep.m_host; + if (ep.m_port != (ep.m_ssl ? 443 : 80)) + res += ":" + std::to_string(ep.m_port); + return res; +} +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.h b/wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.h new file mode 100644 index 0000000000..6eb8c2315d --- /dev/null +++ b/wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.h @@ -0,0 +1,37 @@ +// Copyright 2020 The Beam Team +// +// 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. + +#pragma once +#include +#include + +namespace beam::ethereum +{ +struct RpcEndpoint +{ + bool m_ssl = false; + std::string m_host; // no scheme, no port + uint16_t m_port = 0; // always set (defaulted from scheme) + std::string m_pathAndQuery; // leading '/', "/" if absent +}; + +// Parses an "http://" or "https://" URL: host is a bare hostname/IPv4 or a +// bracketed IPv6 literal, an optional port must be in [1, 65535], and +// embedded credentials (user:pass@) are rejected. Returns false (and leaves +// 'out' untouched) on any violation. +bool ParseEthereumRpcUrl(const std::string& url, RpcEndpoint& out); + +// "https://host[:port]" — safe for logs (path may contain an API key). +std::string SanitizeRpcUrlForLog(const std::string& url); +} diff --git a/wallet/transactions/swaps/bridges/ethereum/settings.cpp b/wallet/transactions/swaps/bridges/ethereum/settings.cpp index 3a2f129329..63cffd2fb9 100644 --- a/wallet/transactions/swaps/bridges/ethereum/settings.cpp +++ b/wallet/transactions/swaps/bridges/ethereum/settings.cpp @@ -14,6 +14,7 @@ #include "settings.h" #include "../../common.h" +#include "rpc_endpoint.h" namespace { const char* get_SwapHashlockContractAddress() { @@ -61,7 +62,14 @@ namespace beam::ethereum { bool Settings::IsInitialized() const { - return m_secretWords.size() == 12 && !m_projectID.empty(); + if (m_secretWords.size() != 12) + return false; + if (m_useCustomRpc) + { + RpcEndpoint ep; + return ParseEthereumRpcUrl(m_customRpcUrl, ep); + } + return !m_projectID.empty(); } bool Settings::IsActivated() const @@ -127,21 +135,49 @@ std::string Settings::GetTokenContractAddress(beam::wallet::AtomicSwapCoin swapC std::string Settings::GetEthNodeAddress() const { + if (m_useCustomRpc) + { + RpcEndpoint ep; + if (ParseEthereumRpcUrl(m_customRpcUrl, ep)) + return ep.m_host + ":" + std::to_string(ep.m_port); + return ""; + } return get_EthNodeAddress(); } std::string Settings::GetEthNodeHost() const { + if (m_useCustomRpc) + { + RpcEndpoint ep; + if (ParseEthereumRpcUrl(m_customRpcUrl, ep)) + return ep.m_host; + return ""; + } return get_EthNodeHost(); } bool Settings::NeedSsl() const { + if (m_useCustomRpc) + { + RpcEndpoint ep; + if (ParseEthereumRpcUrl(m_customRpcUrl, ep)) + return ep.m_ssl; + return true; + } return kNeedSsl; } std::string Settings::GetPathAndQuery() const { - return kPahtAndQuery + m_projectID; // TODO roman.strilets add Project ID + if (m_useCustomRpc) + { + RpcEndpoint ep; + if (ParseEthereumRpcUrl(m_customRpcUrl, ep)) + return ep.m_pathAndQuery; + return "/"; + } + return kPahtAndQuery + m_projectID; } -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/settings.h b/wallet/transactions/swaps/bridges/ethereum/settings.h index a5e1bf6530..3613a8fc83 100644 --- a/wallet/transactions/swaps/bridges/ethereum/settings.h +++ b/wallet/transactions/swaps/bridges/ethereum/settings.h @@ -25,6 +25,8 @@ namespace beam::ethereum struct Settings { std::string m_projectID = ""; + bool m_useCustomRpc = false; + std::string m_customRpcUrl = ""; std::vector m_secretWords = {}; uint32_t m_accountIndex = 0; bool m_shouldConnect = false; @@ -32,7 +34,7 @@ struct Settings uint16_t m_withdrawTxMinConfirmations = 1; uint32_t m_lockTimeInBlocks = 12 * 60 * 4; // 12h double m_blocksPerHour = 250; - Amount m_minFeeRate = wallet::UseMainnetSwap() ? 15u : 1u; + Amount m_minFeeRate = 1u; Amount m_maxFeeRate = 2'000u; uint64_t m_lockTxGasLimit = kLockTxGasLimit; uint64_t m_approveTxGasLimit = kApproveTxGasLimit; @@ -58,6 +60,8 @@ struct Settings bool operator == (const Settings& other) const { return m_projectID == other.m_projectID && + m_useCustomRpc == other.m_useCustomRpc && + m_customRpcUrl == other.m_customRpcUrl && m_secretWords == other.m_secretWords && m_accountIndex == other.m_accountIndex && m_shouldConnect == other.m_shouldConnect; @@ -68,4 +72,4 @@ struct Settings return !(*this == other); } }; -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/settings_provider.cpp b/wallet/transactions/swaps/bridges/ethereum/settings_provider.cpp index 8b789de209..b657765f5c 100644 --- a/wallet/transactions/swaps/bridges/ethereum/settings_provider.cpp +++ b/wallet/transactions/swaps/bridges/ethereum/settings_provider.cpp @@ -31,6 +31,8 @@ void SettingsProvider::SetSettings(const Settings& settings) { // store to DB WriteToDb(GetProjectIDName(), settings.m_projectID); + WriteToDb(GetUseCustomRpcName(), settings.m_useCustomRpc); + WriteToDb(GetCustomRpcUrlName(), settings.m_customRpcUrl); WriteToDb(GetSecretWordsName(), settings.m_secretWords); WriteToDb(GetAccountIndexName(), settings.m_accountIndex); WriteToDb(GetShouldConnectName(), settings.m_shouldConnect); @@ -45,6 +47,8 @@ void SettingsProvider::Initialize() { m_settings = std::make_unique(GetEmptySettings()); ReadFromDB(GetProjectIDName(), m_settings->m_projectID); + ReadFromDB(GetUseCustomRpcName(), m_settings->m_useCustomRpc); + ReadFromDB(GetCustomRpcUrlName(), m_settings->m_customRpcUrl); ReadFromDB(GetSecretWordsName(), m_settings->m_secretWords); ReadFromDB(GetAccountIndexName(), m_settings->m_accountIndex); ReadFromDB(GetShouldConnectName(), m_settings->m_shouldConnect); @@ -84,6 +88,16 @@ std::string SettingsProvider::GetProjectIDName() const return GetSettingsName() + "_ProjectID"; } +std::string SettingsProvider::GetUseCustomRpcName() const +{ + return GetSettingsName() + "_UseCustomRpc"; +} + +std::string SettingsProvider::GetCustomRpcUrlName() const +{ + return GetSettingsName() + "_CustomRpcUrl"; +} + std::string SettingsProvider::GetSecretWordsName() const { return GetSettingsName() + "_SecretWords"; @@ -98,4 +112,4 @@ std::string SettingsProvider::GetShouldConnectName() const { return GetSettingsName() + "_ShouldConnect"; } -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/bridges/ethereum/settings_provider.h b/wallet/transactions/swaps/bridges/ethereum/settings_provider.h index 14fd0fd52f..4a0c9784ee 100644 --- a/wallet/transactions/swaps/bridges/ethereum/settings_provider.h +++ b/wallet/transactions/swaps/bridges/ethereum/settings_provider.h @@ -56,6 +56,8 @@ class SettingsProvider virtual Settings GetEmptySettings(); std::string GetProjectIDName() const; + std::string GetUseCustomRpcName() const; + std::string GetCustomRpcUrlName() const; std::string GetSecretWordsName() const; std::string GetAccountIndexName() const; std::string GetShouldConnectName() const; @@ -86,4 +88,4 @@ class SettingsProvider std::unique_ptr m_settings; size_t m_refCount = 0; }; -} // namespace beam::ethereum \ No newline at end of file +} // namespace beam::ethereum diff --git a/wallet/transactions/swaps/common.cpp b/wallet/transactions/swaps/common.cpp index dec688d3f6..57d55e3843 100644 --- a/wallet/transactions/swaps/common.cpp +++ b/wallet/transactions/swaps/common.cpp @@ -16,12 +16,38 @@ #include "wallet/transactions/swaps/bridges/bitcoin/common.h" #include "wallet/transactions/swaps/bridges/qtum/common.h" #include "bitcoin/bitcoin.hpp" +#include namespace beam::wallet { bool g_EnforceTestnetSwap = false; +bool IsValidEthContractAddress(const std::string& value) +{ + static const std::regex kEthAddressRegex("^0x[0-9a-fA-F]{40}$"); + return std::regex_match(value, kEthAddressRegex); +} + +bool GetValidatedErc20Params(const TxParameters& params, std::string& contract, std::string& symbol, uint8_t& decimals) +{ + auto paramContract = params.GetParameter(TxParameterID::AtomicSwapTokenContract); + auto paramSymbol = params.GetParameter(TxParameterID::AtomicSwapTokenSymbol); + auto paramDecimals = params.GetParameter(TxParameterID::AtomicSwapTokenDecimals); + + if (!paramContract || !IsValidEthContractAddress(*paramContract) || + !paramSymbol || + !paramDecimals || *paramDecimals > kMaxTokenDecimals) + { + return false; + } + + contract = *paramContract; + symbol = *paramSymbol; + decimals = *paramDecimals; + return true; +} + bool UseMainnetSwap() { if (g_EnforceTestnetSwap) @@ -59,6 +85,8 @@ AtomicSwapCoin from_string(const std::string& value) return AtomicSwapCoin::Usdt; else if (value == "wbtc") return AtomicSwapCoin::WBTC; + else if (value == "erc20") + return AtomicSwapCoin::Erc20Token; return AtomicSwapCoin::Unknown; } @@ -82,6 +110,12 @@ uint64_t UnitsPerCoin(AtomicSwapCoin swapCoin) noexcept return 1'000'000'000u; case AtomicSwapCoin::Usdt: return 1'000'000u; + case AtomicSwapCoin::Erc20Token: + case AtomicSwapCoin::ExtendedOffer: + // per-token decimals (AtomicSwapTokenDecimals) replace this fixed table; + // callers must not ask for units-per-coin on these pseudo-coins. + assert(false && "UnitsPerCoin is not defined for Erc20Token/ExtendedOffer"); + return 0; default: { assert("Unsupported swapCoin type."); @@ -136,6 +170,14 @@ std::string GetCoinName(AtomicSwapCoin swapCoin) { return "WBTC"; } + case AtomicSwapCoin::Erc20Token: + { + return "ERC-20"; + } + case AtomicSwapCoin::ExtendedOffer: + { + return "Extended offer"; + } default: { assert(false && "unexpected swap coin!"); @@ -210,6 +252,10 @@ string to_string(beam::wallet::AtomicSwapCoin value) return "USDT"; case beam::wallet::AtomicSwapCoin::WBTC: return "WBTC"; + case beam::wallet::AtomicSwapCoin::Erc20Token: + return "ERC20"; + case beam::wallet::AtomicSwapCoin::ExtendedOffer: + return "EXTENDED"; default: return ""; } diff --git a/wallet/transactions/swaps/common.h b/wallet/transactions/swaps/common.h index d6ed2c4816..a0badfcfae 100644 --- a/wallet/transactions/swaps/common.h +++ b/wallet/transactions/swaps/common.h @@ -58,13 +58,48 @@ enum class AtomicSwapCoin : int32_t // explicit signed type for serialization ba Dai, Usdt, WBTC, + // Appended at the old 'Unknown' ordinal (10); 'Unknown' moves up to 12. + // Wire-format stability: BITCOIN_CASH_SUPPORT does not gate this enum's + // layout (Bitcoin_Cash is unconditional above), so ordinals are identical + // across build configs. The asserts below freeze that invariant. + Erc20Token, + ExtendedOffer, Unknown }; +static_assert(static_cast(AtomicSwapCoin::WBTC) == 9, + "AtomicSwapCoin::WBTC ordinal must stay 9 for wire compatibility"); +static_assert(static_cast(AtomicSwapCoin::Erc20Token) == 10, + "AtomicSwapCoin::Erc20Token takes the old AtomicSwapCoin::Unknown ordinal (10)"); +static_assert(static_cast(AtomicSwapCoin::ExtendedOffer) == 11, + "AtomicSwapCoin::ExtendedOffer ordinal must stay 11"); +static_assert(static_cast(AtomicSwapCoin::Unknown) == 12, + "AtomicSwapCoin::Unknown moved up to 12"); + const AtomicSwapCoin kEthTokens[] = { AtomicSwapCoin::Dai, AtomicSwapCoin::Usdt, AtomicSwapCoin::WBTC }; bool IsEthToken(AtomicSwapCoin swapCoin); +// Upper bound on a per-offer ERC-20 token's on-chain decimals() +// (TxParameterID::AtomicSwapTokenDecimals). decimals is attacker-controlled +// (sourced from the counterparty's contract), so it must be bounded before it +// feeds any wire-amount arithmetic (see ethereum/common.h's +// WalletUnitsPerToken/TokenUnitsMultiplier) and before an offer carrying it is +// accepted onto the offers board (see swap_offers_board.cpp's +// isExtendedOfferDataValid). Defined here (rather than in the ethereum bridge +// headers) so the offers board doesn't need to depend on the ethereum bridge. +constexpr uint8_t kMaxTokenDecimals = 18; + +// Strict 0x-prefixed 20-byte hex address check, shared by the CLI, the API +// and the offers board so the format rule lives in exactly one place. +bool IsValidEthContractAddress(const std::string& value); + +// Extracts the ERC-20 token parameters carried by a swap token/offer, +// validating contract address format and the decimals bound. Shared by the +// CLI and API accept paths (a pasted token may not have gone through the +// offers board's own validation). +bool GetValidatedErc20Params(const TxParameters& params, std::string& contract, std::string& symbol, uint8_t& decimals); + enum class SwapOfferStatus : uint32_t { Pending, diff --git a/wallet/transactions/swaps/lock_tx_builder.cpp b/wallet/transactions/swaps/lock_tx_builder.cpp index effb97cced..820ec78a2f 100644 --- a/wallet/transactions/swaps/lock_tx_builder.cpp +++ b/wallet/transactions/swaps/lock_tx_builder.cpp @@ -102,13 +102,50 @@ namespace beam::wallet << (m_IsSender ? outp.m_Commitment : m_PubKey) >> cp.m_Seed.V; + // asset: both parties derive the same generator blinding from the + // shared seed. The surjection proof's witness is only this scalar, + // not the output blinding, so neither party learns anything extra. + ECC::Point::Native hGen; // unblinded asset generator + ECC::Point::Native hGenBlinded; // hGen + skGen*G, carried by the proof + const ECC::Point::Native* pGen = nullptr; + const ECC::Point::Native* pGenBlinded = nullptr; + ECC::Scalar::Native skSign = m_Sk; + + if (m_AssetID) + { + ECC::Hash::Value hv; + ECC::Hash::Processor() + << "swap.asset.gen" + << cp.m_Seed.V + << m_AssetID + >> hv; + + ECC::Scalar::Native skGen; + ECC::NonceGenerator("swap.asset.sk") + << hv + >> skGen; + + Asset::Base(m_AssetID).get_Generator(hGen); + pGen = &hGen; + + outp.m_pAsset = std::make_unique(); + outp.m_pAsset->Create(m_Height.m_Min, hGenBlinded, skGen, m_AssetID, hGen); + pGenBlinded = &hGenBlinded; + + // the commitment stays v*H_aid + (skA+skB)*G while the rangeproof + // runs on the blinded generator: the sender's proof share absorbs + // the v*skGen skew, so ins/outs/offsets balance as usual + if (m_IsSender) + Asset::Proof::ModifySk(skSign, skGen, cp.m_Value); + } + // commitment ECC::Point::Native pt; if (!pt.Import(outp.m_Commitment)) throw TransactionFailedException(true, TxFailureReason::FailedToCreateMultiSig); pt += m_PubKeyN; - Tag::AddValue(pt, nullptr, cp.m_Value); + Tag::AddValue(pt, pGen, cp.m_Value); pt.Export(outp.m_Commitment); @@ -122,15 +159,15 @@ namespace beam::wallet Oracle o2(o1); uint32_t iVersion = Rules::get().get_BpScheme(m_Height.m_Min); - if (!proof.CoSign(m_SeedSk.V, m_Sk, cp, iVersion, o1, RangeProof::Confidential::Phase::Step2)) + if (!proof.CoSign(m_SeedSk.V, skSign, cp, iVersion, o1, RangeProof::Confidential::Phase::Step2, pGenBlinded)) throw TransactionFailedException(true, TxFailureReason::FailedToCreateMultiSig); if (m_IsSender) { - // complete proof: + // complete proof: GetParameterStrict(TxParameterID::PeerSharedBulletProofPart3, proof.m_Part3); - if (!proof.CoSign(m_SeedSk.V, m_Sk, cp, iVersion, o2, RangeProof::Confidential::Phase::Finalize)) + if (!proof.CoSign(m_SeedSk.V, skSign, cp, iVersion, o2, RangeProof::Confidential::Phase::Finalize, pGenBlinded)) throw TransactionFailedException(true, TxFailureReason::FailedToCreateMultiSig); } else @@ -141,7 +178,7 @@ namespace beam::wallet msig.m_Part2 = proof.m_Part2; ZeroObject(proof.m_Part3); - msig.CoSignPart(m_SeedSk.V, m_Sk, o2, proof.m_Part3); + msig.CoSignPart(m_SeedSk.V, skSign, o2, proof.m_Part3); } } diff --git a/wallet/transactions/swaps/shared_tx_builder.cpp b/wallet/transactions/swaps/shared_tx_builder.cpp index 1bd8d84c89..a79fc5a99c 100644 --- a/wallet/transactions/swaps/shared_tx_builder.cpp +++ b/wallet/transactions/swaps/shared_tx_builder.cpp @@ -38,12 +38,17 @@ namespace beam::wallet bool SharedTxBuilder::AddSharedInput() { - if (m_pTransaction->m_vInputs.empty() && (Status::FullTx != m_Status)) + if (Status::FullTx != m_Status) { Input::Ptr pInp(std::make_unique()); if (!m_Tx.GetParameter(TxParameterID::SharedCommitment, pInp->m_Commitment, SubTxIndex::BEAM_LOCK_TX)) return false; + // the tx may already carry the owner's own fee inputs + for (const auto& p : m_pTransaction->m_vInputs) + if (p->m_Commitment == pInp->m_Commitment) + return true; + m_pTransaction->m_vInputs.push_back(std::move(pInp)); } diff --git a/wallet/transactions/swaps/swap_transaction.cpp b/wallet/transactions/swaps/swap_transaction.cpp index acf8df6581..05e914404f 100644 --- a/wallet/transactions/swaps/swap_transaction.cpp +++ b/wallet/transactions/swaps/swap_transaction.cpp @@ -49,12 +49,23 @@ namespace beam::wallet Height responseTime /*= kDefaultTxResponseTime*/, Height lifetime /*= kDefaultTxLifetime*/) { - auto ownID = db.AllocateKidRange(1); - WalletID wid; - db.get_SbbsWalletID(wid, ownID); + // The offer publisher address must be persisted as an own address: + // SwapOffersBoard::publishOffer() and sendUpdateToNetwork() look up + // the publisher in the set filled from IWalletDB::getAddresses(true), + // otherwise the offer is rejected with ForeignOfferException. A caller + // may stamp the address beforehand (repeated offer-preview + // regeneration must not grow the address book). + if (!params->GetParameter(TxParameterID::MyAddressID) || + !params->GetParameter(TxParameterID::MyAddr)) + { + WalletAddress swapAddr("swap offer"); + db.createAddress(swapAddr); + swapAddr.setExpirationStatus(WalletAddress::ExpirationStatus::Auto); + db.saveAddress(swapAddr); - params->SetParameter(TxParameterID::MyAddressID, ownID); - params->SetParameter(TxParameterID::MyAddr, wid); + params->SetParameter(TxParameterID::MyAddressID, swapAddr.m_OwnID); + params->SetParameter(TxParameterID::MyAddr, swapAddr.m_BbsAddr); + } params->SetParameter(TxParameterID::MinHeight, minHeight); params->SetParameter(TxParameterID::Amount, amount); @@ -113,6 +124,12 @@ namespace beam::wallet copyParameter(TxParameterID::ClientVersion, original, res); copyParameter(TxParameterID::LibraryVersion, original, res); + copyParameter(TxParameterID::AtomicSwapBeamAssetID, original, res); + copyParameter(TxParameterID::AtomicSwapBeamAssetName, original, res); + copyParameter(TxParameterID::AtomicSwapTokenContract, original, res); + copyParameter(TxParameterID::AtomicSwapTokenSymbol, original, res); + copyParameter(TxParameterID::AtomicSwapTokenDecimals, original, res); + if (isOwn) { auto myAddr = *original.GetParameter(TxParameterID::MyAddr); @@ -162,6 +179,12 @@ namespace beam::wallet copyParameter(TxParameterID::ClientVersion, original, res); copyParameter(TxParameterID::LibraryVersion, original, res); + copyParameter(TxParameterID::AtomicSwapBeamAssetID, original, res); + copyParameter(TxParameterID::AtomicSwapBeamAssetName, original, res); + copyParameter(TxParameterID::AtomicSwapTokenContract, original, res); + copyParameter(TxParameterID::AtomicSwapTokenSymbol, original, res); + copyParameter(TxParameterID::AtomicSwapTokenDecimals, original, res); + return res; } @@ -790,6 +813,10 @@ namespace beam::wallet case State::CompleteSwap: { BEAM_LOG_INFO() << GetTxID() << " Swap completed."; + + if (isBeamOwner) + ReleaseUnusedSubTxCoins(SubTxIndex::BEAM_REFUND_TX); + UpdateTxDescription(TxStatus::Completed); GetGateway().on_tx_completed(GetTxID()); break; @@ -836,6 +863,10 @@ namespace beam::wallet case State::Refunded: { BEAM_LOG_INFO() << GetTxID() << " Swap has not succeeded."; + + if (!isBeamOwner) + ReleaseUnusedSubTxCoins(SubTxIndex::BEAM_REDEEM_TX); + UpdateTxDescription(TxStatus::Failed); GetGateway().on_tx_failed(GetTxID()); break; @@ -1150,8 +1181,13 @@ namespace beam::wallet return; } + PropagateBeamAssetID(SubTxIndex::BEAM_LOCK_TX); + m_pLockBuiler = std::make_shared(*this, GetAmount()); m_pLockBuiler->m_IsSender = isBeamOwner; + + if (GetBeamAssetID()) + m_pLockBuiler->VerifyAssetsEnabled(); } LockTxBuilder& builder = *m_pLockBuiler.get(); @@ -1198,7 +1234,10 @@ namespace beam::wallet throw TransactionFailedException(true, TxFailureReason::FailedToGetParameter); } - SetParameter(TxParameterID::Amount, GetAmount() - val, subTxID); + if (GetBeamAssetID() != 0) + SetParameter(TxParameterID::Amount, GetAmount(), subTxID); // full asset amount, the BEAM fee is funded from the owner's own coins + else + SetParameter(TxParameterID::Amount, GetAmount() - val, subTxID); Height h = GetMandatoryParameter(TxParameterID::MinHeight, SubTxIndex::BEAM_LOCK_TX); if (SubTxIndex::BEAM_REFUND_TX == subTxID) @@ -1220,8 +1259,13 @@ namespace beam::wallet if (!SetWithdrawParams(isTxOwner, subTxID)) return; + PropagateBeamAssetID(subTxID); + m_pSharedBuiler = std::make_shared(*this, subTxID); m_pSharedBuiler->m_IsSender = isTxOwner; + + if (GetBeamAssetID()) + m_pSharedBuiler->VerifyAssetsEnabled(); } SharedTxBuilder& builder = *m_pSharedBuiler; @@ -1234,18 +1278,37 @@ namespace beam::wallet assert(builder.m_Coins.IsEmpty()); if (isTxOwner) { + Asset::ID aid = GetBeamAssetID(); + CoinID cid; if (GetParameter(TxParameterID::SharedCoinID, cid)) cid.m_Value = builder.m_Amount; else { - Coin newUtxo = GetWalletDB()->generateNewCoin(builder.m_Amount, 0); + Coin newUtxo = GetWalletDB()->generateNewCoin(builder.m_Amount, aid); cid = newUtxo.m_ID; SetParameter(TxParameterID::SharedCoinID, cid); } builder.m_Coins.m_Output.push_back(cid); + if (aid != 0) + { + // an asset cannot pay the BEAM fee: fund it from the + // owner's own coins, with change + try + { + BaseTxBuilder::Balance bb(builder); + bb.m_Map[0].m_Value -= builder.m_Fee; + bb.CompleteBalance(); + } + catch (const TransactionFailedException&) + { + BEAM_LOG_ERROR() << GetTxID() << "[" << subTxID << "] Withdrawing an asset requires a BEAM balance for the transaction fee"; + throw; + } + } + builder.SaveCoins(); } @@ -1349,7 +1412,7 @@ namespace beam::wallet } } - SetCompletedTxCoinStatuses(hProof); + CompleteSubTxCoinStatuses(subTxID, hProof); return true; } @@ -1378,6 +1441,96 @@ namespace beam::wallet return *m_Amount; } + Asset::ID AtomicSwapTransaction::GetBeamAssetID() const + { + Asset::ID aid = 0; // 0 = BEAM + GetParameter(TxParameterID::AtomicSwapBeamAssetID, aid); + return aid; + } + + void AtomicSwapTransaction::CompleteSubTxCoinStatuses(SubTxID subTxID, Height hProof) + { + // update only this sub-tx's coins: sibling sub-txs (refund vs redeem) + // reserve their own coins and may never be broadcast + auto walletDB = GetWalletDB(); + std::vector modified; + + CoinIDList cids; + if (GetParameter(TxParameterID::InputCoins, cids, subTxID)) + { + for (const auto& cid : cids) + { + Coin c; + c.m_ID = cid; + if (walletDB->findCoin(c)) + { + std::setmin(c.m_spentHeight, hProof); + modified.push_back(c); + } + } + } + + cids.clear(); + if (GetParameter(TxParameterID::OutputCoins, cids, subTxID)) + { + for (const auto& cid : cids) + { + Coin c; + c.m_ID = cid; + if (walletDB->findCoin(c)) + { + std::setmin(c.m_confirmHeight, hProof); + c.m_maturity = hProof; + modified.push_back(c); + } + } + } + + walletDB->saveCoins(modified); + } + + void AtomicSwapTransaction::ReleaseUnusedSubTxCoins(SubTxID subTxID) + { + // the sub-tx was fully built but will never be broadcast: free its coins + auto walletDB = GetWalletDB(); + + CoinIDList cids; + if (GetParameter(TxParameterID::InputCoins, cids, subTxID)) + { + std::vector modified; + for (const auto& cid : cids) + { + Coin c; + c.m_ID = cid; + if (walletDB->findCoin(c)) + { + c.m_spentTxId.reset(); + c.m_spentHeight = MaxHeight; + modified.push_back(c); + } + } + walletDB->saveCoins(modified); + } + + cids.clear(); + if (GetParameter(TxParameterID::OutputCoins, cids, subTxID) && !cids.empty()) + walletDB->removeCoins(cids); + } + + void AtomicSwapTransaction::PropagateBeamAssetID(SubTxID subTxID) + { + Asset::ID aid = GetBeamAssetID(); + if (!aid) + return; + + // every Beam sub-tx must operate on the asset agreed in the offer + Asset::ID cur = 0; + if (!GetParameter(TxParameterID::AssetID, cur, subTxID)) + SetParameter(TxParameterID::AssetID, aid, subTxID); + else if (cur != aid) + throw TransactionFailedException(true, TxFailureReason::InvalidPeerSignature); + } + bool AtomicSwapTransaction::IsSender() const { if (!m_IsSender.is_initialized()) @@ -1524,4 +1677,4 @@ namespace beam::wallet return false; } -} // namespace \ No newline at end of file +} // namespace diff --git a/wallet/transactions/swaps/swap_transaction.h b/wallet/transactions/swaps/swap_transaction.h index abad94002b..8717bed338 100644 --- a/wallet/transactions/swaps/swap_transaction.h +++ b/wallet/transactions/swaps/swap_transaction.h @@ -213,6 +213,10 @@ namespace beam::wallet bool GetKernelFromChain(SubTxID subTxID) const; Amount GetAmount() const; + Asset::ID GetBeamAssetID() const; + void PropagateBeamAssetID(SubTxID subTxID); + void CompleteSubTxCoinStatuses(SubTxID subTxID, Height hProof); + void ReleaseUnusedSubTxCoins(SubTxID subTxID); bool IsSender() const; bool IsBeamSide() const; diff --git a/wallet/transactions/swaps/utils.cpp b/wallet/transactions/swaps/utils.cpp index fbed7a3804..bf5302d050 100644 --- a/wallet/transactions/swaps/utils.cpp +++ b/wallet/transactions/swaps/utils.cpp @@ -212,6 +212,7 @@ void RegisterSwapTxCreators(Wallet::Ptr wallet, IWalletDB::Ptr walletDB) swapTransactionCreator->RegisterFactory(AtomicSwapCoin::Dai, ethFactory); swapTransactionCreator->RegisterFactory(AtomicSwapCoin::Usdt, ethFactory); swapTransactionCreator->RegisterFactory(AtomicSwapCoin::WBTC, ethFactory); + swapTransactionCreator->RegisterFactory(AtomicSwapCoin::Erc20Token, ethFactory); } bool IsLockTxAmountValid( @@ -238,6 +239,7 @@ bool IsLockTxAmountValid( case AtomicSwapCoin::Dai: case AtomicSwapCoin::Usdt: case AtomicSwapCoin::WBTC: + case AtomicSwapCoin::Erc20Token: return true; default: throw std::runtime_error("Unsupported coin for swap"); diff --git a/wallet/unittests/CMakeLists.txt b/wallet/unittests/CMakeLists.txt index 1205a450b0..391e0c38fa 100644 --- a/wallet/unittests/CMakeLists.txt +++ b/wallet/unittests/CMakeLists.txt @@ -62,10 +62,11 @@ endif() if (BEAM_ATOMIC_SWAP_SUPPORT) add_test_snippet(bitcoin_rpc_test swap) - #add_test_snippet(swap_test node swap http wallet_test_node) + add_test_snippet(swap_test node swap http wallet_test_node) add_test_snippet(swap_board_test swap_offers_board node) add_test_snippet(electrum_test swap) #add_test_snippet(ethereum_test swap) + add_test_snippet(eth_rpc_endpoint_test swap) endif() if (BEAM_LASER_SUPPORT) diff --git a/wallet/unittests/bitcoin_rpc_test.cpp b/wallet/unittests/bitcoin_rpc_test.cpp index 152cb9ccb8..3edb1d7132 100644 --- a/wallet/unittests/bitcoin_rpc_test.cpp +++ b/wallet/unittests/bitcoin_rpc_test.cpp @@ -20,6 +20,7 @@ #include "wallet/transactions/swaps/bridges/bitcoin/bitcoin_core_016.h" #include "wallet/transactions/swaps/bridges/bitcoin/settings_provider.h" +#include "wallet/transactions/swaps/bridges/bitcoin/common.h" #include "test_helpers.h" @@ -89,7 +90,7 @@ void testSuccessResponse() auto settingsProvider = std::make_shared(btcUserName, btcPass, addr); bitcoin::BitcoinCore016 bridge = bitcoin::BitcoinCore016(*reactor, *settingsProvider); - bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos) + bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos, Amount fee) { WALLET_CHECK(error.m_type == bitcoin::IBridge::None); WALLET_CHECK(!tx.empty()); @@ -205,7 +206,7 @@ void testEmptyResult() auto settingsProvider = std::make_shared(btcUserName, btcPass, addr); bitcoin::BitcoinCore016 bridge = bitcoin::BitcoinCore016(*reactor, *settingsProvider); - bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos) + bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos, Amount fee) { WALLET_CHECK(error.m_type == bitcoin::IBridge::EmptyResult); WALLET_CHECK(!error.m_message.empty()); @@ -259,7 +260,7 @@ void testEmptyResponse() auto settingsProvider = std::make_shared(btcUserName, btcPass, addr); bitcoin::BitcoinCore016 bridge = bitcoin::BitcoinCore016(*reactor, *settingsProvider); - bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos) + bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos, Amount fee) { WALLET_CHECK(error.m_type == bitcoin::IBridge::InvalidResultFormat); WALLET_CHECK(!error.m_message.empty()); @@ -286,7 +287,7 @@ void testConnectionRefused() auto settingsProvider = std::make_shared(btcUserName, btcPass, addr); bitcoin::BitcoinCore016 bridge = bitcoin::BitcoinCore016(*reactor, *settingsProvider); - bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos) + bridge.fundRawTransaction("", 2, [&counter](const bitcoin::IBridge::Error& error, const std::string& tx, int pos, Amount fee) { WALLET_CHECK(error.m_type == bitcoin::IBridge::IOError); WALLET_CHECK(!error.m_message.empty()); @@ -297,6 +298,53 @@ void testConnectionRefused() WALLET_CHECK(counter == 1); } +void TestFundedTxFeeSufficiency() +{ + // build a 2-in/2-out dummy tx with libbitcoin, serialize to hex + libbitcoin::chain::transaction tx; + tx.set_version(2); + for (int i = 0; i < 2; ++i) + { + libbitcoin::chain::input in; + tx.inputs().push_back(in); + } + libbitcoin::chain::output out(100000, libbitcoin::chain::script()); + tx.outputs().push_back(out); + tx.outputs().push_back(out); + std::string hex = libbitcoin::encode_base16(tx.to_data()); + + // each unsigned input already serializes at kUnsignedInputVsize; the gate + // tops it up to the signed P2WPKH lower bound + beam::Amount vsize = tx.serialized_size() + (bitcoin::kMinInputVsize - bitcoin::kUnsignedInputVsize) * 2; + beam::Amount rate = 1000; // sat/kB + beam::Amount exact = (vsize * rate) / 1000u; + + WALLET_CHECK(bitcoin::IsFundedTxFeeSufficient(hex, exact, rate)); + WALLET_CHECK(bitcoin::IsFundedTxFeeSufficient(hex, exact + 1, rate)); + WALLET_CHECK(!bitcoin::IsFundedTxFeeSufficient(hex, exact - 1, rate)); + WALLET_CHECK(bitcoin::IsFundedTxFeeSufficient("zzzz-not-hex", 0, rate)); // undecodable -> permissive + + // single-input tx: exercises the per-input multiplier at n=1 + libbitcoin::chain::transaction tx1; + tx1.set_version(2); + libbitcoin::chain::input in1; + tx1.inputs().push_back(in1); + tx1.outputs().push_back(out); + tx1.outputs().push_back(out); + std::string hex1 = libbitcoin::encode_base16(tx1.to_data()); + + beam::Amount vsize1 = tx1.serialized_size() + (bitcoin::kMinInputVsize - bitcoin::kUnsignedInputVsize) * 1; + beam::Amount exact1 = (vsize1 * rate) / 1000u; + + WALLET_CHECK(bitcoin::IsFundedTxFeeSufficient(hex1, exact1, rate)); + WALLET_CHECK(bitcoin::IsFundedTxFeeSufficient(hex1, exact1 + 1, rate)); + WALLET_CHECK(!bitcoin::IsFundedTxFeeSufficient(hex1, exact1 - 1, rate)); + + // valid hex, but too short to parse as a transaction: from_data fails, + // and the permissive fallback lets the sign/broadcast path report it. + WALLET_CHECK(bitcoin::IsFundedTxFeeSufficient("00", 0, rate)); +} + thread_local const beam::Rules* beam::Rules::s_pInstance = nullptr; int main() @@ -312,6 +360,7 @@ int main() testEmptyResult(); testEmptyResponse(); testConnectionRefused(); + TestFundedTxFeeSufficiency(); assert(g_failureCount == 0); return WALLET_CHECK_RESULT; diff --git a/wallet/unittests/eth_rpc_endpoint_test.cpp b/wallet/unittests/eth_rpc_endpoint_test.cpp new file mode 100644 index 0000000000..6943b0a0a8 --- /dev/null +++ b/wallet/unittests/eth_rpc_endpoint_test.cpp @@ -0,0 +1,213 @@ +#include +#include "test_helpers.h" +#include "wallet/transactions/swaps/bridges/ethereum/rpc_endpoint.h" +#include "wallet/transactions/swaps/bridges/ethereum/settings.h" +#include "wallet/transactions/swaps/bridges/ethereum/common.h" +#include "wallet/transactions/swaps/utils.h" + +WALLET_TEST_INIT + +thread_local const beam::Rules* beam::Rules::s_pInstance = nullptr; + +using namespace beam::ethereum; + +namespace +{ +RpcEndpoint parseOk(const std::string& url) +{ + RpcEndpoint ep; + WALLET_CHECK(ParseEthereumRpcUrl(url, ep)); + return ep; +} + +void checkReject(const std::string& url) +{ + RpcEndpoint ep; + WALLET_CHECK(!ParseEthereumRpcUrl(url, ep)); +} +} + +void TestAccepts() +{ + auto ep = parseOk("https://mainnet.infura.io/v3/abcdef0123456789"); + WALLET_CHECK(ep.m_ssl); + WALLET_CHECK(ep.m_host == "mainnet.infura.io"); + WALLET_CHECK(ep.m_port == 443); + WALLET_CHECK(ep.m_pathAndQuery == "/v3/abcdef0123456789"); + + ep = parseOk("http://localhost:8545"); + WALLET_CHECK(!ep.m_ssl); + WALLET_CHECK(ep.m_host == "localhost"); + WALLET_CHECK(ep.m_port == 8545); + WALLET_CHECK(ep.m_pathAndQuery == "/"); + + ep = parseOk("http://192.168.1.20:8545"); + WALLET_CHECK(ep.m_host == "192.168.1.20"); + WALLET_CHECK(ep.m_port == 8545); + + ep = parseOk("HTTPS://ETH-mainnet.g.alchemy.com/v2/MyKey_123-x"); + WALLET_CHECK(ep.m_ssl && ep.m_port == 443); + WALLET_CHECK(ep.m_host == "eth-mainnet.g.alchemy.com"); // host lowercased + WALLET_CHECK(ep.m_pathAndQuery == "/v2/MyKey_123-x"); // path case preserved + + ep = parseOk(" http://node.internal:8545/rpc?key=1 "); // outer whitespace trimmed + WALLET_CHECK(ep.m_pathAndQuery == "/rpc?key=1"); + + ep = parseOk("http://plainhost"); // default port from scheme + WALLET_CHECK(ep.m_port == 80); +} + +void TestRejects() +{ + checkReject(""); + checkReject("mainnet.infura.io/v3/abc"); // scheme-less + // IPv6 literals: the transport is IPv4-only, the parser must not accept + // what can never connect + checkReject("http://[::1]:8545"); + checkReject("http://[2001:db8::1]:8545"); + checkReject("http://[::ffff:192.0.2.1]:8545"); + checkReject("ftp://host/path"); + checkReject("file:///etc/passwd"); + checkReject("ws://host:8546"); + checkReject("https://user:pass@host/path"); // embedded credentials + checkReject("http://host:0"); // port out of range + checkReject("http://host:65536"); + checkReject("http://host:8545/pa th"); // inner whitespace + checkReject(std::string("http://host/\x01path")); // control char + checkReject("http://"); // empty host + checkReject("http://host:notaport"); + checkReject("http://[not*valid$stuff]:8545"); // invalid bracket content + checkReject("http://[::1"); // unterminated bracket + checkReject("http://[]:8545"); // empty bracket content + checkReject("http://[::1/foo]:8545"); // '/' before closing bracket +} + +void TestSanitize() +{ + WALLET_CHECK(SanitizeRpcUrlForLog("https://mainnet.infura.io/v3/SECRET") == "https://mainnet.infura.io"); + WALLET_CHECK(SanitizeRpcUrlForLog("http://localhost:8545/key") == "http://localhost:8545"); + WALLET_CHECK(SanitizeRpcUrlForLog("garbage") == ""); +} + +void TestSettingsAccessors() +{ + Settings s; + s.m_projectID = "abc"; + WALLET_CHECK(s.GetPathAndQuery() == "/v3/abc"); + WALLET_CHECK(s.NeedSsl()); + + s.m_useCustomRpc = true; + s.m_customRpcUrl = "http://localhost:8545/rpc"; + WALLET_CHECK(s.GetEthNodeAddress() == "localhost:8545"); + WALLET_CHECK(s.GetEthNodeHost() == "localhost"); + WALLET_CHECK(!s.NeedSsl()); + WALLET_CHECK(s.GetPathAndQuery() == "/rpc"); + + Settings t = s; + WALLET_CHECK(s == t); + t.m_customRpcUrl = "http://other:8545"; + WALLET_CHECK(s != t); // endpoint change must trigger bridge reset +} + +void TestPerTokenUnitsHelpers() +{ + // ETH/DAI: 18 decimals -> today's constants (UnitsPerCoin == 10^9, GetCoinUnitsMultiplier == 10^9) + WALLET_CHECK(WalletUnitsPerToken(18) == 1'000'000'000ULL); + WALLET_CHECK(TokenUnitsMultiplier(18) == 1'000'000'000u); + + // USDT: 6 decimals -> today's constants (UnitsPerCoin == 10^6, GetCoinUnitsMultiplier == 1) + WALLET_CHECK(WalletUnitsPerToken(6) == 1'000'000ULL); + WALLET_CHECK(TokenUnitsMultiplier(6) == 1u); + + // WBTC: 8 decimals -> today's constants (UnitsPerCoin == satoshi_per_bitcoin == 10^8, GetCoinUnitsMultiplier == 1) + WALLET_CHECK(WalletUnitsPerToken(8) == 100'000'000ULL); + WALLET_CHECK(TokenUnitsMultiplier(8) == 1u); + + // 0 decimals -> both sides collapse to the identity + WALLET_CHECK(WalletUnitsPerToken(0) == 1ULL); + WALLET_CHECK(TokenUnitsMultiplier(0) == 1u); + + // 9 decimals -> boundary: all precision fits the wallet Amount, no on-wire scaling needed + WALLET_CHECK(WalletUnitsPerToken(9) == 1'000'000'000ULL); + WALLET_CHECK(TokenUnitsMultiplier(9) == 1u); + + // kMaxTokenDecimals (18) -> boundary of the allowed range, still exact + WALLET_CHECK(WalletUnitsPerToken(18) == 1'000'000'000ULL); + WALLET_CHECK(TokenUnitsMultiplier(18) == 1'000'000'000u); + + // NOTE: decimals > kMaxTokenDecimals (e.g. 19, 255) is intentionally not exercised + // here via a direct call: WalletUnitsPerToken/TokenUnitsMultiplier assert(decimals + // <= kMaxTokenDecimals) before clamping, and this suite builds with assertions + // enabled (Debug), so such a call aborts the process before the clamped value could + // ever be observed - that's the intended fail-fast for a programmer error reaching + // these helpers directly. The real defense against an attacker-controlled decimals + // is upstream, at the two points that can observe a value coming from a + // counterparty/peer: SwapOffersBoard::isExtendedOfferDataValid (board_test.cpp) and + // EthereumBridge::getTokenInfo's decode. The clamp math itself (10^min(decimals,9) + // and 10^max(0,decimals-9) evaluated at decimals=kMaxTokenDecimals) is covered by + // the kMaxTokenDecimals boundary case above. +} + +void TestIsLockTxAmountValidErc20() +{ + using namespace beam::wallet; + + // Erc20Token must route like the other ethereum-based coins (receiver pays + // fee), not fall through to the "unsupported coin" default and throw. + WALLET_CHECK(IsLockTxAmountValid(AtomicSwapCoin::Erc20Token, 1, 1)); + WALLET_CHECK(IsLockTxAmountValid(AtomicSwapCoin::Erc20Token, 0, 0)); + + // A classic (non-ethereum-based) coin's result must be unchanged. + WALLET_CHECK(!IsLockTxAmountValid(AtomicSwapCoin::Bitcoin, 1, 1)); +} + +namespace +{ +std::string makeWord(const std::string& lowByteHex, const std::string& highBytesHex = std::string(62, '0')) +{ + return highBytesHex + lowByteHex; +} +} + +void TestParseTokenDecimalsWord() +{ + uint8_t decimals = 0xFF; + + // Accepted values: proper 64-char words, only the low byte set. + WALLET_CHECK(ParseTokenDecimalsWord(makeWord("00"), decimals) && decimals == 0); + WALLET_CHECK(ParseTokenDecimalsWord(makeWord("06"), decimals) && decimals == 6); + WALLET_CHECK(ParseTokenDecimalsWord(makeWord("08"), decimals) && decimals == 8); + WALLET_CHECK(ParseTokenDecimalsWord(makeWord("09"), decimals) && decimals == 9); + WALLET_CHECK(ParseTokenDecimalsWord(makeWord("12"), decimals) && decimals == 18); + + // Rejected: value beyond kMaxTokenDecimals (18). + WALLET_CHECK(!ParseTokenDecimalsWord(makeWord("13"), decimals)); // 19 + WALLET_CHECK(!ParseTokenDecimalsWord(makeWord("ff"), decimals)); // 255 + + // Rejected: a high byte is set. + WALLET_CHECK(!ParseTokenDecimalsWord(makeWord("06", std::string(60, '0') + "01"), decimals)); + + // Rejected: wrong length. + WALLET_CHECK(!ParseTokenDecimalsWord("06", decimals)); + WALLET_CHECK(!ParseTokenDecimalsWord(makeWord("06") + "00", decimals)); + WALLET_CHECK(!ParseTokenDecimalsWord(std::string(), decimals)); + + // Rejected: non-hex characters. + WALLET_CHECK(!ParseTokenDecimalsWord(std::string(62, '0') + "zz", decimals)); +} + +int main() +{ + beam::Rules r; + beam::Rules::Scope scopeRules(r); + + std::cout << "Ethereum RPC endpoint parser tests:" << std::endl; + TestAccepts(); + TestRejects(); + TestSanitize(); + TestSettingsAccessors(); + TestPerTokenUnitsHelpers(); + TestIsLockTxAmountValidErc20(); + TestParseTokenDecimalsWord(); + return WALLET_CHECK_RESULT; +} diff --git a/wallet/unittests/swap_board_test.cpp b/wallet/unittests/swap_board_test.cpp index ab0a053445..86ff2b2a7f 100644 --- a/wallet/unittests/swap_board_test.cpp +++ b/wallet/unittests/swap_board_test.cpp @@ -21,9 +21,12 @@ WALLET_TEST_INIT // tested module #include "wallet/client/extensions/broadcast_gateway/broadcast_router.h" #include "wallet/client/extensions/offers_board/swap_offers_board.h" +#include "wallet/transactions/swaps/swap_transaction.h" +#include "wallet/transactions/swaps/utils.h" // dependencies #include "keykeeper/local_private_key_keeper.h" +#include "utility/hex.h" #include @@ -381,6 +384,157 @@ namespace cout << "Test end" << endl; } + void TestRawErc20WireCoinRejected() + { + cout << endl << "Test raw Erc20Token wire coin is rejected" << endl; + + // A legitimate publisher never emits AtomicSwapCoin::Erc20Token as the + // top-level wire coin - it always substitutes ExtendedOffer (see + // SwapOffersBoard::broadcastOffer / SwapOffer::IsExtended). So craft + // the wire message directly (bypassing publishOffer's substitution) + // to simulate a malformed/malicious peer sending raw Erc20Token. + auto storage = createSqliteWalletDB(); + WalletAddress wa; + storage->createAddress(wa); + storage->saveAddress(wa); + + OfferBoardProtocolHandler protocolHandler(storage->get_SbbsKdf()); + auto mockNetwork = MockBbsNetwork::CreateInstance(); + BroadcastRouter broadcastRouter(mockNetwork, *mockNetwork, MockTimestampHolder::CreateInstance()); + SwapOffersBoard Alice(broadcastRouter, protocolHandler, storage); + + HeightHash startState; + startState.m_Height = Fork1Height; + Alice.onSystemStateChanged(startState); + + WALLET_CHECK(Alice.getOffersList().size() == 0); + + SwapOffer offer = createOffer(generateTxID(), SwapOfferStatus::Pending, wa.m_BbsAddr, AtomicSwapCoin::Erc20Token, true); + + BroadcastMsg msg; + WALLET_CHECK_NO_THROW(msg = protocolHandler.createBroadcastMessage(offer, wa.m_OwnID)); + + broadcastRouter.sendMessage(BroadcastContentType::SwapOffers, msg); + + WALLET_CHECK(Alice.getOffersList().size() == 0); + + // Delivery control: the same board/router wiring must accept a + // CLASSIC offer, proving the rejection above isn't a broken-delivery + // artifact. + SwapOffer classicOffer = createOffer(generateTxID(), SwapOfferStatus::Pending, wa.m_BbsAddr, AtomicSwapCoin::Bitcoin, true); + + BroadcastMsg classicMsg; + WALLET_CHECK_NO_THROW(classicMsg = protocolHandler.createBroadcastMessage(classicOffer, wa.m_OwnID)); + + broadcastRouter.sendMessage(BroadcastContentType::SwapOffers, classicMsg); + + WALLET_CHECK(Alice.getOffersList().size() == 1); + + cout << "Test end" << endl; + } + + void TestExtendedErc20OfferDecimalsBounded() + { + cout << endl << "Test extended Erc20Token offer decimals bound (kMaxTokenDecimals)" << endl; + + // decimals is attacker-controlled: it's carried in a peer's offer-board + // message (TxParameterID::AtomicSwapTokenDecimals), sourced from the + // counterparty's ERC-20 contract. isExtendedOfferDataValid() must drop an + // extended Erc20Token offer whose decimals exceeds kMaxTokenDecimals, + // otherwise it feeds ethereum::TokenUnitsMultiplier's unbounded + // 10^(decimals-9) math downstream. + auto storage = createSqliteWalletDB(); + WalletAddress wa; + storage->createAddress(wa); + storage->saveAddress(wa); + + OfferBoardProtocolHandler protocolHandler(storage->get_SbbsKdf()); + auto mockNetwork = MockBbsNetwork::CreateInstance(); + BroadcastRouter broadcastRouter(mockNetwork, *mockNetwork, MockTimestampHolder::CreateInstance()); + SwapOffersBoard Alice(broadcastRouter, protocolHandler, storage); + + HeightHash startState; + startState.m_Height = Fork1Height; + Alice.onSystemStateChanged(startState); + + WALLET_CHECK(Alice.getOffersList().size() == 0); + + auto makeErc20Offer = [&](uint8_t decimals) + { + // AtomicSwapCoin param carries the real coin (Erc20Token); the wire coin + // is overridden to ExtendedOffer afterwards to mirror what + // SwapOffersBoard::broadcastOffer does for a legitimate publisher (see + // TestRawErc20WireCoinRejected above for why a raw wire Erc20Token is + // instead rejected outright, before isExtendedOfferDataValid ever runs). + SwapOffer offer = createOffer(generateTxID(), SwapOfferStatus::Pending, wa.m_BbsAddr, AtomicSwapCoin::Erc20Token, true); + offer.SetParameter(TxParameterID::AtomicSwapTokenContract, std::string("0x0000000000000000000000000000000000000001")); + offer.SetParameter(TxParameterID::AtomicSwapTokenSymbol, std::string("TKN")); + offer.SetParameter(TxParameterID::AtomicSwapTokenDecimals, decimals); + offer.m_coin = AtomicSwapCoin::ExtendedOffer; + return offer; + }; + + { + cout << "\tCase: decimals == kMaxTokenDecimals + 1 (19) is rejected" << endl; + SwapOffer offer = makeErc20Offer(kMaxTokenDecimals + 1); + + BroadcastMsg msg; + WALLET_CHECK_NO_THROW(msg = protocolHandler.createBroadcastMessage(offer, wa.m_OwnID)); + broadcastRouter.sendMessage(BroadcastContentType::SwapOffers, msg); + + WALLET_CHECK(Alice.getOffersList().size() == 0); + } + { + cout << "\tCase: decimals == kMaxTokenDecimals (18) is accepted" << endl; + SwapOffer offer = makeErc20Offer(kMaxTokenDecimals); + + BroadcastMsg msg; + WALLET_CHECK_NO_THROW(msg = protocolHandler.createBroadcastMessage(offer, wa.m_OwnID)); + broadcastRouter.sendMessage(BroadcastContentType::SwapOffers, msg); + + WALLET_CHECK(Alice.getOffersList().size() == 1); + + // Params must survive the accept round trip intact. + auto offersList = Alice.getOffersList(); + const SwapOffer& received = offersList.front(); + WALLET_CHECK(received.ResolveCoin() == AtomicSwapCoin::Erc20Token); + auto contract = received.GetParameter(TxParameterID::AtomicSwapTokenContract); + WALLET_CHECK(contract && *contract == "0x0000000000000000000000000000000000000001"); + auto symbol = received.GetParameter(TxParameterID::AtomicSwapTokenSymbol); + WALLET_CHECK(symbol && *symbol == "TKN"); + auto receivedDecimals = received.GetParameter(TxParameterID::AtomicSwapTokenDecimals); + WALLET_CHECK(receivedDecimals && *receivedDecimals == kMaxTokenDecimals); + } + { + cout << "\tCase: malformed contract address (not 0x + 40 hex) is rejected" << endl; + SwapOffer offer = makeErc20Offer(kMaxTokenDecimals); + offer.SetParameter(TxParameterID::AtomicSwapTokenContract, std::string("not-a-contract-address")); + + BroadcastMsg msg; + WALLET_CHECK_NO_THROW(msg = protocolHandler.createBroadcastMessage(offer, wa.m_OwnID)); + broadcastRouter.sendMessage(BroadcastContentType::SwapOffers, msg); + + // still just the single valid offer accepted in the case above + WALLET_CHECK(Alice.getOffersList().size() == 1); + } + { + cout << "\tCase: oversized/garbage symbol is rejected" << endl; + SwapOffer offer = makeErc20Offer(kMaxTokenDecimals); + // 33 chars (over the 32-char bound) with a non-printable byte mixed in. + std::string garbageSymbol(33, 'X'); + garbageSymbol[5] = '\x01'; + offer.SetParameter(TxParameterID::AtomicSwapTokenSymbol, garbageSymbol); + + BroadcastMsg msg; + WALLET_CHECK_NO_THROW(msg = protocolHandler.createBroadcastMessage(offer, wa.m_OwnID)); + broadcastRouter.sendMessage(BroadcastContentType::SwapOffers, msg); + + WALLET_CHECK(Alice.getOffersList().size() == 1); + } + + cout << "Test end" << endl; + } + void TestCommunication() { cout << endl << "Test boards communication and notification" << endl; @@ -883,6 +1037,298 @@ namespace } } + void TestFillSwapTxParamsPublish() + { + cout << endl << "Test FillSwapTxParams produces publishable offer" << endl; + + auto storage = createSqliteWalletDB(); + + auto mockNetwork = MockBbsNetwork::CreateInstance(); + BroadcastRouter broadcastRouter(mockNetwork, *mockNetwork, MockTimestampHolder::CreateInstance()); + OfferBoardProtocolHandler protocolHandler(storage->get_SbbsKdf()); + SwapOffersBoard board(broadcastRouter, protocolHandler, storage); + + HeightHash startState; + startState.m_Height = Fork1Height; + board.onSystemStateChanged(startState); + + // The board learns about newly saved addresses via IWalletDbObserver; + // subscribe so the publisher address created below is picked up + // (mirrors how the real wallet client wires SwapOffersBoard). + storage->Subscribe(&board); + + // Create the offer exactly as UI/CLI/API do: via FillSwapTxParams. + auto params = CreateSwapTransactionParameters(generateTxID()); + FillSwapTxParams(¶ms, + *storage, + Fork1Height, // minHeight + 1000, // amount + 100, // beamFee + AtomicSwapCoin::Bitcoin, + 2000, // swapAmount + 10, // swapFeeRate + true); // isBeamSide + + SwapOffer offer(*params.GetTxID()); + offer.SetTxParameters(params.Pack()); + offer.m_status = SwapOfferStatus::Pending; + offer.m_coin = AtomicSwapCoin::Bitcoin; + offer.m_publisherId = *params.GetParameter(TxParameterID::MyAddr); + + size_t offersReceived = 0; + MockBoardObserver observer([&offersReceived](ChangeAction action, const vector& offers) { + if (action == ChangeAction::Added) offersReceived += offers.size(); + }); + board.Subscribe(&observer); + + // A publisher address that was never saved makes the board reject the + // offer as foreign ("Offer has foreign Pk and will not be published"). + WALLET_CHECK_NO_THROW(board.publishOffer(offer)); + WALLET_CHECK(offersReceived == 1); + + board.Unsubscribe(&observer); + storage->Unsubscribe(&board); + } + + void TestMirrorAndTokenizeCarryTokenParams() + { + cout << endl << "Test MirrorSwapTxParams/PrepareSwapTxParamsForTokenization carry Erc20 token params" << endl; + + // MirrorSwapTxParams and PrepareSwapTxParamsForTokenization must carry + // AtomicSwapTokenContract/Symbol/Decimals and AtomicSwapBeamAssetID/Name + // through unchanged, or accept/publish-offer round trips silently drop + // the extended-offer data. + auto storage = createSqliteWalletDB(); + + auto params = CreateSwapTransactionParameters(generateTxID()); + FillSwapTxParams(¶ms, + *storage, + Fork1Height, // minHeight + 1000, // amount + 100, // beamFee + AtomicSwapCoin::Erc20Token, + 2000, // swapAmount + 10, // swapFeeRate + true); // isBeamSide + + const std::string kContract = "0x000000000000000000000000000000000000ab"; + const std::string kSymbol = "TKN"; + const uint8_t kDecimals = 6; + const Asset::ID kAssetId = 7; + const std::string kAssetName = "TEST"; + + params.SetParameter(TxParameterID::AtomicSwapTokenContract, kContract); + params.SetParameter(TxParameterID::AtomicSwapTokenSymbol, kSymbol); + params.SetParameter(TxParameterID::AtomicSwapTokenDecimals, kDecimals); + params.SetParameter(TxParameterID::AtomicSwapBeamAssetID, kAssetId); + params.SetParameter(TxParameterID::AtomicSwapBeamAssetName, kAssetName); + + auto checkTokenParamsSurvived = [&](const TxParameters& result, const char* stage) + { + cout << "\tStage: " << stage << endl; + auto contract = result.GetParameter(TxParameterID::AtomicSwapTokenContract); + auto symbol = result.GetParameter(TxParameterID::AtomicSwapTokenSymbol); + auto decimals = result.GetParameter(TxParameterID::AtomicSwapTokenDecimals); + auto assetId = result.GetParameter(TxParameterID::AtomicSwapBeamAssetID); + auto assetName = result.GetParameter(TxParameterID::AtomicSwapBeamAssetName); + + WALLET_CHECK(contract && *contract == kContract); + WALLET_CHECK(symbol && *symbol == kSymbol); + WALLET_CHECK(decimals && *decimals == kDecimals); + WALLET_CHECK(assetId && *assetId == kAssetId); + WALLET_CHECK(assetName && *assetName == kAssetName); + }; + + auto mirrored = MirrorSwapTxParams(params, true); + checkTokenParamsSurvived(mirrored, "MirrorSwapTxParams"); + + auto tokenized = PrepareSwapTxParamsForTokenization(params); + checkTokenParamsSurvived(tokenized, "PrepareSwapTxParamsForTokenization"); + + cout << "Test end" << endl; + } + + void TestClassicOfferWireStability() + { + cout << endl << "Test classic offer wire (SwapOfferToken) byte stability" << endl; + + // Locks the wire format that old wallets parse for a classic + // (non-extended) offer's m_coin/params. + // + // Two non-obvious facts shape this test: + // - createBroadcastMessage's signature is NOT deterministic: ECC:: + // SignatureBase::CreateNonces (core/ecc.cpp) mixes GenRandom() into + // the nonce, so msg.m_signature differs between calls even for the + // identical SwapOffer. msg.m_content (== toByteBuffer(SwapOfferToken + // (offer))) is stable though, so the golden asserts on + // SwapOfferToken's serialized content rather than the signed + // BroadcastMsg. + // - A DB-derived WalletID (via generateTestOffer/createSqliteWalletDB) + // is also not run-to-run stable: WalletDB::AllocateKidRange + // (wallet/core/wallet_db.cpp) seeds the 'LastKid' counter from + // beam::getTimestamp() when the DB has no prior value, so the + // publisher WalletID's m_Channel differs on every fresh-DB run. + // The golden below therefore uses a hardcoded TxID/WalletID instead + // of generateTestOffer's DB-backed address. + // + // If the serialization format ever legitimately changes, regenerate by + // temporarily printing the actual hex here and pasting it back in. + TxID txID; + for (uint8_t i = 0; i < 16; ++i) txID[i] = i + 1; + + WalletID publisherId; + publisherId.m_Channel = 0x1122334455667788ULL; + for (uint8_t i = 0; i < 32; ++i) publisherId.m_Pk.m_pData[i] = i + 1; + + SwapOffer offer(txID, SwapOfferStatus::Pending, publisherId, AtomicSwapCoin::Bitcoin, true); + offer.SetParameter(TxParameterID::AtomicSwapCoin, offer.m_coin); + offer.SetParameter(TxParameterID::AtomicSwapIsBeamSide, true); + offer.SetParameter(TxParameterID::Amount, Amount(12345)); + offer.SetParameter(TxParameterID::AtomicSwapAmount, Amount(6789)); + offer.SetParameter(TxParameterID::MinHeight, Height(Fork1Height)); + offer.SetParameter(TxParameterID::PeerResponseTime, Height(10)); + offer.SetParameter(TxParameterID::TransactionType, TxType::AtomicSwap); + + const std::string kGoldenHex = + "010102030405060708090a0b0c0d0e0f1001800111223344556677880102030405" + "060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20014087010082" + "01010102830239300104818a0118818a011e8101011f814001208302851a"; + + ByteBuffer tokenBytes = toByteBuffer(SwapOfferToken(offer)); + const std::string actualHex = to_hex(tokenBytes.data(), tokenBytes.size()); + WALLET_CHECK(actualHex == kGoldenHex); + + // Same offer serialized twice must also be stable (rules out any + // hidden per-call nondeterminism in SwapOfferToken/Pack itself). + ByteBuffer tokenBytes2 = toByteBuffer(SwapOfferToken(offer)); + WALLET_CHECK(tokenBytes == tokenBytes2); + + cout << "Test end" << endl; + } + + void TestExtendedOfferCompat() + { + cout << endl << "Test extended-offer (CA / Erc20) wire compat and old-wallet-guard drop" << endl; + + // An unmodified old wallet's onOfferFromNetwork rejects any offer whose + // m_coin is >= the old AtomicSwapCoin::Unknown ordinal, which was 10 + // (see common.h's static_asserts: Erc20Token now takes that old ordinal, + // Unknown moved to 12). ExtendedOffer's ordinal is 11: >= 10 (old Unknown) + // so the old guard drops it; but see below re. >= 12 being false. + constexpr int32_t kOldUnknownOrdinal = 10; + static_assert(static_cast(AtomicSwapCoin::ExtendedOffer) >= kOldUnknownOrdinal, + "an older peer's 'm_coin >= old Unknown (10)' guard must drop ExtendedOffer (11), " + "which is why the board wire-tags extended offers with it instead of the real coin"); + static_assert(!(static_cast(AtomicSwapCoin::ExtendedOffer) >= 12), + "ExtendedOffer (11) must stay below the NEW Unknown ordinal (12): it's a valid, " + "intentionally-produced wire value on the new wallet side, not itself an unknown coin"); + + auto storage = createSqliteWalletDB(); + WalletAddress wa; + storage->createAddress(wa); + storage->saveAddress(wa); + + OfferBoardProtocolHandler protocolHandler(storage->get_SbbsKdf()); + auto mockNetwork = MockBbsNetwork::CreateInstance(); + BroadcastRouter routerAlice(mockNetwork, *mockNetwork, MockTimestampHolder::CreateInstance()); + BroadcastRouter routerBob(mockNetwork, *mockNetwork, MockTimestampHolder::CreateInstance()); + BroadcastRouter routerEve(mockNetwork, *mockNetwork, MockTimestampHolder::CreateInstance()); + + SwapOffersBoard Alice(routerAlice, protocolHandler, storage); + SwapOffersBoard Bob(routerBob, protocolHandler, storage); + + HeightHash startState; + startState.m_Height = Fork1Height; + Alice.onSystemStateChanged(startState); + Bob.onSystemStateChanged(startState); + + // Eve doesn't run a SwapOffersBoard; she just snoops the raw broadcast + // traffic on the SwapOffers content type to observe the actual wire + // m_coin Alice's board puts on the message (as opposed to Bob's already- + // resolved, post-onOfferFromNetwork copy). + boost::optional lastWireOffer; + MockBroadcastListener eve( + [&lastWireOffer, &protocolHandler] + (BroadcastMsg& msg) + { + lastWireOffer = protocolHandler.parseMessage(msg); + }); + routerEve.registerListener(BroadcastContentType::SwapOffers, &eve); + + auto findByTxId = [](const SwapOffersBoard& board, const TxID& txId) -> boost::optional + { + for (auto& o : board.getOffersList()) + { + if (o.m_txId == txId) + { + return o; + } + } + return boost::none; + }; + + // generateTxID() reseeds std::rand from time(nullptr) (1s resolution) + // on every call, so two calls within the same wall-clock second produce + // an identical TxID and the second publishOffer collides with the first + // (OfferAlreadyPublishedException) - use one base TxID and increment for + // the second case instead of calling generateTxID() twice. + TxID txID = generateTxID(); + + { + cout << "\tCase: CA offer (AtomicSwapBeamAssetID=7, foreign coin Bitcoin)" << endl; + + SwapOffer offer = createOffer(txID, SwapOfferStatus::Pending, wa.m_BbsAddr, AtomicSwapCoin::Bitcoin, true); + offer.SetParameter(TxParameterID::AtomicSwapBeamAssetID, Asset::ID(7)); + offer.SetParameter(TxParameterID::AtomicSwapBeamAssetName, std::string("MyCoolAsset")); + + lastWireOffer.reset(); + PublishOfferNoThrow(Alice, offer); + + // Wire assertion: what actually went out on the wire is ExtendedOffer, + // never the real (Bitcoin) coin. + WALLET_CHECK(lastWireOffer); + WALLET_CHECK(lastWireOffer->m_coin == AtomicSwapCoin::ExtendedOffer); + + // Receive-side assertion: Bob's cached/observed copy resolves back to + // the real foreign coin, with the CA params intact. + auto received = findByTxId(Bob, offer.m_txId); + WALLET_CHECK(received); + WALLET_CHECK(received->ResolveCoin() == AtomicSwapCoin::Bitcoin); + auto beamAssetId = received->GetParameter(TxParameterID::AtomicSwapBeamAssetID); + WALLET_CHECK(beamAssetId && *beamAssetId == Asset::ID(7)); + auto beamAssetName = received->GetParameter(TxParameterID::AtomicSwapBeamAssetName); + WALLET_CHECK(beamAssetName && *beamAssetName == "MyCoolAsset"); + } + { + cout << "\tCase: Erc20 extended offer (foreign coin Erc20Token)" << endl; + + SwapOffer offer = createOffer(++txID, SwapOfferStatus::Pending, wa.m_BbsAddr, AtomicSwapCoin::Erc20Token, true); + offer.SetParameter(TxParameterID::AtomicSwapTokenContract, std::string("0x0000000000000000000000000000000000000002")); + offer.SetParameter(TxParameterID::AtomicSwapTokenSymbol, std::string("XYZ")); + offer.SetParameter(TxParameterID::AtomicSwapTokenDecimals, uint8_t(6)); + + lastWireOffer.reset(); + PublishOfferNoThrow(Alice, offer); + + WALLET_CHECK(lastWireOffer); + WALLET_CHECK(lastWireOffer->m_coin == AtomicSwapCoin::ExtendedOffer); + + auto received = findByTxId(Bob, offer.m_txId); + WALLET_CHECK(received); + WALLET_CHECK(received->ResolveCoin() == AtomicSwapCoin::Erc20Token); + auto contract = received->GetParameter(TxParameterID::AtomicSwapTokenContract); + WALLET_CHECK(contract && *contract == "0x0000000000000000000000000000000000000002"); + auto symbol = received->GetParameter(TxParameterID::AtomicSwapTokenSymbol); + WALLET_CHECK(symbol && *symbol == "XYZ"); + auto decimals = received->GetParameter(TxParameterID::AtomicSwapTokenDecimals); + WALLET_CHECK(decimals && *decimals == uint8_t(6)); + } + + routerEve.unregisterListener(BroadcastContentType::SwapOffers); + + cout << "Test end" << endl; + } + } // namespace thread_local const beam::Rules* beam::Rules::s_pInstance = nullptr; @@ -906,11 +1352,17 @@ int main() TestProtocolHandlerIntegration(); TestMandatoryParameters(); + TestRawErc20WireCoinRejected(); + TestExtendedErc20OfferDecimalsBounded(); + TestClassicOfferWireStability(); + TestExtendedOfferCompat(); TestCommunication(); TestLinkedTransactionChanges(); TestDelayedOfferUpdate(); TestOffersLifetimeCheck(); TestOwnOfferCheck(); + TestFillSwapTxParamsPublish(); + TestMirrorAndTokenizeCarryTokenParams(); boost::filesystem::remove(dbFileName); diff --git a/wallet/unittests/swap_test.cpp b/wallet/unittests/swap_test.cpp index 57d3d6c04a..2f9bdffbb0 100644 --- a/wallet/unittests/swap_test.cpp +++ b/wallet/unittests/swap_test.cpp @@ -1,2138 +1,2571 @@ -// Copyright 2019 The Beam Team -// -// 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. - -#include "wallet/core/common.h" -#include "wallet/core/wallet_network.h" -#include "wallet/core/wallet.h" -#include "wallet/core/simple_transaction.h" -#include "keykeeper/local_private_key_keeper.h" -#include "wallet/core/secstring.h" -#include "wallet/transactions/swaps/common.h" -#include "wallet/transactions/swaps/swap_transaction.h" -#include "wallet/transactions/swaps/utils.h" -#include "wallet/transactions/swaps/second_side.h" -#include "wallet/transactions/swaps/bridges/bitcoin/bitcoin.h" -#include "wallet/transactions/swaps/bridges/ethereum/ethereum.h" - -#include "http/http_client.h" -#include "utility/test_helpers.h" -#include "core/radixtree.h" -#include "core/unittest/mini_blockchain.h" -#include "core/negotiator.h" -#include "node/node.h" -#include "utility/io/sslserver.h" - -#include "test_helpers.h" -#include "wallet_test_node.h" - -#include -#include -#include -#include - -using namespace beam; -using namespace std; -using namespace ECC; - -WALLET_TEST_INIT - -#include "wallet_test_environment.cpp" -#include "swap_test_environment.cpp" - -namespace -{ - const AmountList kDefaultTestAmounts = { 500, 200, 100, 900 }; - const Height kNodeStartHeight = 145; - const uint16_t kBtcTxMinConfirmations = 2; - const uint32_t kLockTimeInBlocks = 100; - - TestBitcoinWallet GetSenderBTCWallet(io::Reactor& reactor, const io::Address& senderAddress, Amount swapAmount) - { - TestBitcoinWallet::Options senderOptions; - senderOptions.m_rawAddress = "2N8N2kr34rcGqHCo3aN6yqniid8a4Mt3FCv"; - senderOptions.m_privateKey = "cSFMca7FAeAgLRgvev5ajC1v1jzprBr1KoefUFFPS8aw3EYwLArM"; - senderOptions.m_refundTx = "0200000001809fc0890cb2724a941dfc3b7213a63b3017b0cddbed4f303be300cb55ddca830100000000ffffffff01e8030000000000001976a9146ed612a79317bc6ade234f299073b945ccb3e76b88ac00000000"; - senderOptions.m_amount = swapAmount; - - return TestBitcoinWallet(reactor, senderAddress, senderOptions); - } - - TestBitcoinWallet GetReceiverBTCWallet(io::Reactor& reactor, const io::Address& receiverAddress, Amount swapAmount) - { - TestBitcoinWallet::Options receiverOptions; - receiverOptions.m_rawAddress = "2Mvfsv3JiwWXjjwNZD6LQJD4U4zaPAhSyNB"; - receiverOptions.m_privateKey = "cNoRPsNczFw6b7wTuwLx24gSnCPyF3CbvgVmFJYKyfe63nBsGFxr"; - receiverOptions.m_refundTx = "0200000001809fc0890cb2724a941dfc3b7213a63b3017b0cddbed4f303be300cb55ddca830100000000ffffffff01e8030000000000001976a9146ed612a79317bc6ade234f299073b945ccb3e76b88ac00000000"; - receiverOptions.m_amount = swapAmount; - - return TestBitcoinWallet(reactor, receiverAddress, receiverOptions); - } - - TxParameters AcceptSwapParameters(const TxParameters& initialParameters, Amount fee, Amount feeRate) - { - TxParameters parameters = initialParameters; - - parameters.SetParameter(TxParameterID::PeerAddr, *parameters.GetParameter(TxParameterID::MyAddr)); - parameters.DeleteParameter(TxParameterID::MyAddr); - parameters.DeleteParameter(TxParameterID::MyAddressID); - - bool isBeamSide = !*parameters.GetParameter(TxParameterID::AtomicSwapIsBeamSide); - - if (isBeamSide) - { - // delete parameters from other side - parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::BEAM_REDEEM_TX); - parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::LOCK_TX); - parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::REFUND_TX); - - // add our parameters - parameters.SetParameter(TxParameterID::Fee, fee, SubTxIndex::BEAM_LOCK_TX); - parameters.SetParameter(TxParameterID::Fee, fee, SubTxIndex::BEAM_REFUND_TX); - parameters.SetParameter(TxParameterID::Fee, feeRate, SubTxIndex::REDEEM_TX); - } - else - { - // delete parameters from other side - parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::BEAM_LOCK_TX); - parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::BEAM_REFUND_TX); - parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::REDEEM_TX); - - // add our parameters - parameters.SetParameter(TxParameterID::Fee, fee, SubTxIndex::BEAM_REDEEM_TX); - parameters.SetParameter(TxParameterID::Fee, feeRate, SubTxIndex::LOCK_TX); - parameters.SetParameter(TxParameterID::Fee, feeRate, SubTxIndex::REFUND_TX); - } - - parameters.SetParameter(TxParameterID::IsSender, isBeamSide); - parameters.SetParameter(TxParameterID::AtomicSwapIsBeamSide, isBeamSide); - parameters.SetParameter(TxParameterID::IsInitiator, true); - - return parameters; - } - - class TestSettings : public bitcoin::Settings - { - public: - TestSettings() - { - SetLockTxMinConfirmations(kBtcTxMinConfirmations); - SetLockTimeInBlocks(kLockTimeInBlocks); - } - }; - - TxParameters InitNewSwap2( - const TestWalletRig& twr, Height minHeight, Amount amount, - Amount fee, AtomicSwapCoin swapCoin, Amount swapAmount, Amount swapFee, - bool isBeamSide = true, Height lifetime = kDefaultTxLifetime, - Height responseTime = kDefaultTxResponseTime) - { - return InitNewSwap(*twr.m_WalletDB, minHeight, amount, fee, swapCoin, swapAmount, swapFee, isBeamSide, lifetime, responseTime); - } - -} - -bitcoin::ISettingsProvider::Ptr InitSettingsProvider(IWalletDB::Ptr walletDB, const TestSettings& settings) -{ - auto settingsProvider = std::make_shared(walletDB); - settingsProvider->SetSettings(settings); - return settingsProvider; -} - -void InitBitcoin(Wallet& wallet, IWalletDB::Ptr walletDB, io::Reactor& reactor, bitcoin::ISettingsProvider& settingsProvider) -{ - auto creator = std::make_shared(walletDB); - auto bridge = std::make_shared(reactor, settingsProvider); - // TODO should refactored this code - auto bitcoinBridgeCreator = [bridge]() -> bitcoin::IBridge::Ptr - { - return bridge; - }; - auto factory = wallet::MakeSecondSideFactory(bitcoinBridgeCreator, settingsProvider); - creator->RegisterFactory(AtomicSwapCoin::Bitcoin, factory); - wallet.RegisterTransactionType(TxType::AtomicSwap, std::static_pointer_cast(creator)); -} - -void InitElectrum(Wallet& wallet, IWalletDB::Ptr walletDB, io::Reactor& reactor, bitcoin::ISettingsProvider& settingsProvider) -{ - auto creator = std::make_shared(walletDB); - auto bridge = std::make_shared(reactor, settingsProvider); - // TODO should refactored this code - auto bitcoinBridgeCreator = [bridge]() -> bitcoin::IBridge::Ptr - { - return bridge; - }; - auto factory = wallet::MakeSecondSideFactory(bitcoinBridgeCreator, settingsProvider); - creator->RegisterFactory(AtomicSwapCoin::Bitcoin, factory); - wallet.RegisterTransactionType(TxType::AtomicSwap, std::static_pointer_cast(creator)); -} - -void TestSwapTransaction(bool isBeamOwnerStart, beam::Height fork1Height) -{ - cout << "\nTesting atomic swap transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completeAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 101; - Amount swapAmount = 2000; - Amount feeRate = 256; - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - - TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - InitBitcoin(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - receiverBtcWallet.addPeer(senderAddress); - - TxID txID = { {0} }; - - auto receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - Node node; - - NodeObserver observer([&]() - { - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == fork1Height + 5) - { - auto currentHeight = cursor.m_Sid.m_Height; - bool isBeamSide = !isBeamOwnerStart; - auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, - currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, isBeamSide); - - TestWalletRig* initiator = &sender; - TestWalletRig* acceptor = &receiver; - if (isBeamOwnerStart) - { - std::swap(initiator, acceptor); - } - - initiator->m_Wallet->StartTransaction(parameters); - auto acceptParams = AcceptSwapParameters(parameters, beamFee, feeRate); - - txID = acceptor->m_Wallet->StartTransaction(acceptParams); - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 200); - - mainReactor->run(); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.size() == 1); - WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); - WALLET_CHECK(receiverCoins[0].m_createTxId == txID); - - auto senderCoins = sender.GetCoins(); - WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - // change - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - - // check secret - NoLeak senderSecretPrivateKey; - storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, senderSecretPrivateKey.V); - NoLeak receiverSecretPrivateKey; - storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, receiverSecretPrivateKey.V); - WALLET_CHECK(senderSecretPrivateKey.V != Zero && senderSecretPrivateKey.V == receiverSecretPrivateKey.V); -} - -void TestElectrumSwapTransaction(bool isBeamOwnerStart, beam::Height fork1Height) -{ - cout << "\nTesting atomic swap transaction on electrum...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completeAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - std::string address("127.0.0.1:10400"); - - Amount beamAmount = 300; - Amount beamFee = 102; - Amount swapAmount = 200000; - Amount feeRate = 80000; - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - - TestSettings bobSettings; - bobSettings.SetElectrumConnectionOptions({ address, {"unveil", "shadow", "gold", "piece", "salad", "parent", "leisure", "obtain", "wave", "eternal", "suggest", "artwork"}, false}); - - TestSettings aliceSettings; - aliceSettings.SetElectrumConnectionOptions({ address, {"rib", "genuine", "fury", "advance", "train", "capable", "rough", "silk", "march", "vague", "notice", "sphere"}, false}); - - TestElectrumWallet btcWallet(*mainReactor, address); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - InitElectrum(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); - InitElectrum(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); - - TxID txID = { {0} }; - - auto receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - Node node; - - NodeObserver observer([&]() - { - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == fork1Height + 5) - { - auto currentHeight = cursor.m_Sid.m_Height; - bool isBeamSide = !isBeamOwnerStart; - auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, - currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, isBeamSide); - - if (isBeamOwnerStart) - { - receiver.m_Wallet->StartTransaction(parameters); - txID = sender.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - } - else - { - sender.m_Wallet->StartTransaction(parameters); - txID = receiver.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - } - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 200); - - mainReactor->run(); - - receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.size() == 1); - WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); - WALLET_CHECK(receiverCoins[0].m_createTxId == txID); - - auto senderCoins = sender.GetCoins(); - WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - // change - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - - // check secret - NoLeak senderSecretPrivateKey; - storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, senderSecretPrivateKey.V); - NoLeak receiverSecretPrivateKey; - storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, receiverSecretPrivateKey.V); - WALLET_CHECK(senderSecretPrivateKey.V != Zero && senderSecretPrivateKey.V == receiverSecretPrivateKey.V); -} - -void TestSwapTransactionWithoutChange(bool isBeamOwnerStart) -{ - cout << "\nTesting atomic swap transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completeAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 380; - Amount beamFee = 120; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - InitBitcoin(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); - - receiverBtcWallet.addPeer(senderAddress); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - TxID txID = { {0} }; - - if (isBeamOwnerStart) - { - auto parameters = InitNewSwap2(receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver.m_Wallet->StartTransaction(parameters); - txID = sender.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - } - else - { - auto parameters = InitNewSwap2(sender, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, true); - sender.m_Wallet->StartTransaction(parameters); - txID = receiver.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - } - - auto receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - mainReactor->run(); - - receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.size() == 1); - WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); - WALLET_CHECK(receiverCoins[0].m_createTxId == txID); - - auto senderCoins = sender.GetCoins(); - WALLET_CHECK(senderCoins.size() == 4); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); -} - -void TestSwapBTCRefundTransaction() -{ - cout << "\nAtomic swap: testing BTC refund transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - receiverBtcWallet.addPeer(senderAddress); - - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - io::AsyncEvent::Ptr eventToUpdate; - uint64_t startBlocks = receiverBtcWallet.getBlockCount(); - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate, &timer]() - { - if (sender) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) - { - // delete sender to simulate refund on BTC side - sender.reset(); - } - eventToUpdate->post(); - } - else - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState != wallet::AtomicSwapTransaction::State::SendingRefundTX) - { - // speed-up test - timer->restart(50, true); - } - } - }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate receiver TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.size() == 0); - WALLET_CHECK(receiverBtcWallet.getBlockCount() - startBlocks >= kLockTimeInBlocks); - - // TODO: add check BTC balance -} - -void TestSwapBTCQuickRefundTransaction() -{ - cout << "\nAtomic swap: testing BTC quick refund transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - receiverBtcWallet.addPeer(senderAddress); - - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, Wallet::TxCompletedAction(), TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - io::AsyncEvent::Ptr eventToUpdate; - bool isCanceled = false; - uint64_t startBlocks = receiverBtcWallet.getBlockCount(); - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate, &timer, &isCanceled]() - { - if (!isCanceled) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) - { - sender->m_Wallet->CancelTransaction(txID); - isCanceled = true; - } - eventToUpdate->post(); - } - else - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState != wallet::AtomicSwapTransaction::State::SendingRefundTX) - { - // speed-up test - timer->restart(50, true); - } - } - }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate receiver TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.size() == 0); - WALLET_CHECK(receiverBtcWallet.getBlockCount() - startBlocks < kLockTimeInBlocks); -} - -void TestElectrumSwapBTCRefundTransaction() -{ - cout << "\nAtomic swap: testing BTC refund transaction on electrum...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - std::string address("127.0.0.1:10400"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 200000; - Amount feeRate = 80000; - - TestSettings bobSettings; - bobSettings.SetElectrumConnectionOptions({ address, {"unveil", "shadow", "gold", "piece", "salad", "parent", "leisure", "obtain", "wave", "eternal", "suggest", "artwork"}, false}); - - TestSettings aliceSettings; - aliceSettings.SetElectrumConnectionOptions({ address, {"rib", "genuine", "fury", "advance", "train", "capable", "rough", "silk", "march", "vague", "notice", "sphere"}, false}); - - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - TestElectrumWallet btcWallet(*mainReactor, address); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - InitElectrum(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitElectrum(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - - receiver->m_Wallet->StartTransaction(parameters); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - io::AsyncEvent::Ptr eventToUpdate; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate, &timer]() - { - if (sender) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) - { - // delete sender to simulate refund on BTC side - sender.reset(); - } - eventToUpdate->post(); - } - else - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState != wallet::AtomicSwapTransaction::State::SendingRefundTX) - { - // speed-up test - timer->restart(50, true); - } - } - }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate receiver TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.size() == 0); -} - -void TestSwapBeamRefundTransaction() -{ - cout << "\nAtomic swap: testing Beam refund transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 350; - Amount beamFee = 125; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - receiverBtcWallet.addPeer(senderAddress); - - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - cout << "receiver tx start" << endl; - receiver->m_Wallet->StartTransaction(parameters); - cout << "sender tx start" << endl; - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - cout << "waiting..." << endl; - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::AsyncEvent::Ptr eventToUpdate; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, &receiver, txID, &eventToUpdate, &node]() - { - if (receiver) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) - { - cout << "deleting rcvr" << endl; - - // delete receiver to simulate refund on Beam side - receiver.reset(); - } - eventToUpdate->post(); - } - else - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState != wallet::AtomicSwapTransaction::State::SendingBeamRefundTX) - { - // speed-up test - node.AddBlock(); - eventToUpdate->post(); - } - } - }); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate sender TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == 6); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - - // change of Beam LockTx - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - - // Refund - WALLET_CHECK(senderCoins[5].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(senderCoins[5].m_status == Coin::Available); - WALLET_CHECK(senderCoins[5].m_createTxId == txID); -} - -void TestSwapBeamAndBTCRefundTransaction() -{ - cout << "\nAtomic swap: testing Beam and BTC refund transactions...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completedAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 350; - Amount beamFee = 125; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - receiverBtcWallet.addPeer(senderAddress); - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - vector receiverCoins; - io::AsyncEvent::Ptr eventToUpdate; - bool isNeedReset = true, bTxStarted = false; - Node node; - TxID txID; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() - { - eventToUpdate->post(); - - if (!bTxStarted) - return; - - if (receiver && isNeedReset) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) - { - cout << "Deleting receiver" << endl; - - // delete receiver to simulate refund on Beam side - receiver.reset(); - isNeedReset = false; - node.m_Cfg.m_TestMode.m_FakePowSolveTime_ms = 500; - } - } - else - { - Height minHeight; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::MinHeight, minHeight); - Height currentHeight = sender->m_WalletDB->getCurrentHeight(); - - if (currentHeight - minHeight > 5 * 60 && !receiver) - { - cout << "Restoring receiver" << endl; - - receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - receiver->m_Wallet->ResumeAllTransactions(); - } - } - }); - - - NodeObserver observer([&]() - { - Height minHeight = 15; - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == minHeight) - { - cout << "Starting tx" << endl; - - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - auto parameters = InitNewSwap2(*receiver, minHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - bTxStarted = true; - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 500); - - - eventToUpdate->post(); - mainReactor->run(); - - // validate sender TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == 6); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - - // change of Beam LockTx - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - - // Refund - WALLET_CHECK(senderCoins[5].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(senderCoins[5].m_status == Coin::Available); - WALLET_CHECK(senderCoins[5].m_createTxId == txID); - - txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); -} - -void TestSwapBTCRedeemAfterExpired() -{ - cout << "\nAtomic swap: testing BTC redeem after Beam expired...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completedAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 350; - Amount beamFee = 125; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - receiverBtcWallet.addPeer(senderAddress); - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - vector receiverCoins; - io::AsyncEvent::Ptr eventToUpdate; - bool isNeedReset = true; - Node node; - TxID txID; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() - { - if (sender && isNeedReset) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) - { - sender.reset(); - isNeedReset = false; - node.m_Cfg.m_TestMode.m_FakePowSolveTime_ms = 500; - } - } - else - { - Height minHeight; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::MinHeight, minHeight); - Height currentHeight = receiver->m_WalletDB->getCurrentHeight(); - - if (currentHeight - minHeight > 6 * 60 && !sender) - { - sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - sender->m_Wallet->ResumeAllTransactions(); - } - - if (currentHeight - minHeight > 500) - { - mainReactor->stop(); - } - } - eventToUpdate->post(); - }); - - - NodeObserver observer([&]() - { - Height minHeight = 15; - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == minHeight) - { - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - auto parameters = InitNewSwap2(*receiver, minHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 500); - - - eventToUpdate->post(); - mainReactor->run(); - - // validate sender TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::CompleteSwap); - - txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::CompleteSwap); -} - -void TestElectrumSwapBeamRefundTransaction() -{ - cout << "\nAtomic swap: testing Beam refund transaction on electrum...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - std::string address("127.0.0.1:10400"); - - Amount beamAmount = 320; - Amount beamFee = 110; - Amount swapAmount = 200000; - Amount feeRate = 80000; - - TestSettings bobSettings; - bobSettings.SetElectrumConnectionOptions({ address, {"unveil", "shadow", "gold", "piece", "salad", "parent", "leisure", "obtain", "wave", "eternal", "suggest", "artwork"}, false}); - - TestSettings aliceSettings; - aliceSettings.SetElectrumConnectionOptions({ address, {"rib", "genuine", "fury", "advance", "train", "capable", "rough", "silk", "march", "vague", "notice", "sphere"}, false}); - - TestElectrumWallet btcWallet(*mainReactor, address); - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - InitElectrum(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitElectrum(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::AsyncEvent::Ptr eventToUpdate; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, &receiver, txID, &eventToUpdate, &node]() - { - if (receiver) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) - { - // delete receiver to simulate refund on Beam side - receiver.reset(); - } - eventToUpdate->post(); - } - else - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState != wallet::AtomicSwapTransaction::State::SendingBeamRefundTX) - { - // speed-up test - node.AddBlock(); - eventToUpdate->post(); - } - } - }); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate sender TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == 6); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - - // change of Beam LockTx - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - - // Refund - WALLET_CHECK(senderCoins[5].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(senderCoins[5].m_status == Coin::Available); - WALLET_CHECK(senderCoins[5].m_createTxId == txID); -} - -void ExpireByResponseTime(bool isBeamSide) -{ - // Simulate swap transaction without response from second side - - cout << "\nAtomic swap: testing expired transaction on " << (isBeamSide ? "Beam" : "BTC") << " side...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 2000; - Amount feeRate = 256; - Height lifetime = 100; - Height responseTime = 100; - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", senderAddress }); - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, completedAction); - - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - - auto db = createReceiverWalletDB(); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - auto swapParameters = InitNewSwap(*db, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, !isBeamSide, lifetime, responseTime); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(swapParameters, beamFee, feeRate)); - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(50, true, [&node]() {node.AddBlock(); }); - - mainReactor->run(); - - // validate sender TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Failed); - - TxFailureReason reason = TxFailureReason::Unknown; - storage::getTxParameter(*sender->m_WalletDB, txID, TxParameterID::InternalFailureReason, reason); - WALLET_CHECK(reason == TxFailureReason::TransactionExpired); - - if (isBeamSide) - { - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == 4); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Available); - } -} - -void TestSwapCancelTransaction(bool isSender, wallet::AtomicSwapTransaction::State testingState) -{ - cout << "\nAtomic swap: testing cancel transaction (" << (isSender ? "sender" : "receiver") << ", " << wallet::getSwapTxStatus(testingState) << ")...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completedAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, isSender ? Wallet::TxCompletedAction() : completedAction); - auto receiver = std::make_unique(receiverWalletDB, isSender ? completedAction : Wallet::TxCompletedAction()); - - receiverBtcWallet.addPeer(senderAddress); - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - TxID txID = receiver->m_Wallet->StartTransaction(parameters); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - bool bStarted = false; - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(1000, true, [&node, &bStarted, &sender, &receiver, ¶meters, beamFee, feeRate]() - { - node.AddBlock(); - - if (!bStarted && receiver->m_Wallet->IsWalletInSync() && sender->m_Wallet->IsWalletInSync()) - { - sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - bStarted = true; - io::Reactor::get_Current().stop(); - } - }); - - mainReactor->run(); - WALLET_CHECK(bStarted); - - - io::AsyncEvent::Ptr eventToUpdate; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&walletRig = isSender ? sender: receiver, testingState, txID, &eventToUpdate]() - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*walletRig->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == testingState) - { - walletRig->m_Wallet->CancelTransaction(txID); - } - else - { - eventToUpdate->post(); - } - }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate sender TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == (isSender ? wallet::AtomicSwapTransaction::State::Canceled : wallet::AtomicSwapTransaction::State::Failed)); - - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == (isSender ? wallet::AtomicSwapTransaction::State::Failed : wallet::AtomicSwapTransaction::State::Canceled)); - - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == 4); - - for (const auto& coin : senderCoins) - { - WALLET_CHECK(coin.m_status == Coin::Available); - } -} - -void TestExpireByLifeTime() -{ - cout << "\nAtomic swap: expire by lifetime ...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completeAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - auto receiverWalletDB = createReceiverWalletDB(); - auto senderWalletDB = createSenderWalletDB(0, 0); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - auto sender = std::make_unique(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_unique(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - receiverBtcWallet.addPeer(senderAddress); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - bool isNeedReset = true; - io::AsyncEvent::Ptr eventToUpdate; - Node node; - TxID txID; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() - { - if (receiver) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::Failed) - { - return; - } - if (txState == wallet::AtomicSwapTransaction::State::BuildingBeamLockTX && isNeedReset) - { - receiver.reset(); - isNeedReset = false; - } - } - else if (sender) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::Failed) - { - receiver = std::make_unique(receiverWalletDB, completeAction); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - receiver->m_Wallet->ResumeAllTransactions(); - } - } - eventToUpdate->post(); - }); - - eventToUpdate->post(); - - NodeObserver observer([&]() - { - Height minHeight = 5; - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == minHeight) - { - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - auto parameters = InitNewSwap2(*receiver, minHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 200); - - mainReactor->run(); - - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Failed); - - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Failed); - - TxFailureReason reason = TxFailureReason::Unknown; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, TxParameterID::InternalFailureReason, reason); - WALLET_CHECK(reason == TxFailureReason::TransactionExpired); - - reason = TxFailureReason::Unknown; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, TxParameterID::InternalFailureReason, reason); - WALLET_CHECK(reason == TxFailureReason::TransactionExpired); - - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size()); - - for (size_t i = 0; i < kDefaultTestAmounts.size(); i++) - { - WALLET_CHECK(senderCoins[i].m_status == Coin::Available); - WALLET_CHECK(senderCoins[i].m_ID.m_Value == kDefaultTestAmounts[i]); - } -} - -void TestIgnoringThirdPeer() -{ - cout << "\nAtomic swap: testing ignoring of third peer\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completedAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 100; - Amount swapAmount = 2000; - Amount feeRate = 256; - - TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); - - TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); - receiverBtcWallet.addPeer(senderAddress); - - auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); - - InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); - receiver->m_Wallet->StartTransaction(parameters); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(200, true, [&node]() {node.AddBlock(); }); - - io::AsyncEvent::Ptr eventToUpdate; - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() - { - WalletID peerID; - bool result = storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::PeerAddr, peerID); - if (result) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::BuildingBeamLockTX) - { - // create new address - WalletAddress newAddress; - sender->m_WalletDB->createAddress(newAddress); - sender->m_WalletDB->saveAddress(newAddress); - - // send msg from new address - SetTxParameter msg; - msg.AddParameter(TxParameterID::SubTxIndex, SubTxIndex::BEAM_REFUND_TX) - .AddParameter(TxParameterID::PeerSignature, ECC::Scalar::Native()); - - msg.m_TxID = txID; - msg.m_Type = wallet::TxType::AtomicSwap; - msg.m_From = newAddress.m_BbsAddr; - - sender->m_messageEndpoint->Send(receiver->m_BbsAddr, msg); - return; - } - } - eventToUpdate->post(); - }); - - eventToUpdate->post(); - mainReactor->run(); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.size() == 1); - WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); - WALLET_CHECK(receiverCoins[0].m_createTxId == txID); - - auto senderCoins = sender->GetCoins(); - WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - // change - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); -} - -namespace beam::ethereum -{ - class Provider : public ISettingsProvider - { - public: - Provider(const Settings& settings) - : m_settings(settings) - { - } - - Settings GetSettings() const override - { - return m_settings; - } - - void SetSettings(const Settings& settings) override - { - m_settings = settings; - } - - bool CanModify() const override - { - return true; - } - - void AddRef() override - { - } - - void ReleaseRef() override - { - - } - - private: - Settings m_settings; - }; -} - -ethereum::ISettingsProvider::Ptr InitSettingsProvider(IWalletDB::Ptr walletDB, const ethereum::Settings& settings) -{ - auto settingsProvider = std::make_shared(walletDB); - settingsProvider->SetSettings(settings); - return settingsProvider; -} - -void InitEthereum(Wallet& wallet, IWalletDB::Ptr walletDB, io::Reactor& reactor, ethereum::ISettingsProvider& settingsProvider) -{ - auto creator = std::make_shared(walletDB); - auto bridge = std::make_shared(reactor, settingsProvider); - // TODO should refactored this code - auto bridgeCreator = [bridge]() -> ethereum::IBridge::Ptr - { - return bridge; - }; - auto factory = wallet::MakeSecondSideFactory(bridgeCreator, settingsProvider); - creator->RegisterFactory(AtomicSwapCoin::Ethereum, factory); - creator->RegisterFactory(AtomicSwapCoin::Dai, factory); - wallet.RegisterTransactionType(TxType::AtomicSwap, std::static_pointer_cast(creator)); -} - -// TODO roman.strilets need to implement new test -void TestEthSwapTransaction(bool isBeamOwnerStart, beam::Height fork1Height) -{ - cout << "\nTesting ethereum atomic swap transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completeAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - io::Address senderAddress; - senderAddress.resolve("127.0.0.1:10400"); - - io::Address receiverAddress; - receiverAddress.resolve("127.0.0.1:10300"); - - Amount beamAmount = 300; - Amount beamFee = 101; - Amount swapAmount = 2'000'000'000u; - Amount gasPrice = 30u; - //Amount feeRate = 256; - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - - ethereum::Settings aliceSettings; - aliceSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; - aliceSettings.m_accountIndex = 6; - //aliceSettings.m_address = "127.0.0.1:7545"; - aliceSettings.m_shouldConnect = true; - aliceSettings.m_lockTxMinConfirmations = 2; - //aliceSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; - - ethereum::Settings bobSettings; - bobSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; - bobSettings.m_accountIndex = 5; - //bobSettings.m_address = "127.0.0.1:7545"; - bobSettings.m_shouldConnect = true; - bobSettings.m_lockTxMinConfirmations = 2; - //bobSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; - - /*TestSettings bobSettings; - bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); - - TestSettings aliceSettings; - aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress });*/ - - /*TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); - TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount);*/ - - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - - TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - InitEthereum(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); - InitEthereum(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - //receiverBtcWallet.addPeer(senderAddress); - - TxID txID = { {0} }; - - auto receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - Node node; - - NodeObserver observer([&]() - { - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == fork1Height + 5) - { - auto currentHeight = cursor.m_Sid.m_Height; - bool isBeamSide = !isBeamOwnerStart; - auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, - currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Ethereum, swapAmount, - gasPrice, isBeamSide); - - TestWalletRig* initiator = &sender; - TestWalletRig* acceptor = &receiver; - if (isBeamOwnerStart) - { - std::swap(initiator, acceptor); - } - - initiator->m_Wallet->StartTransaction(parameters); - auto acceptParams = AcceptSwapParameters(parameters, beamFee, gasPrice); - - txID = acceptor->m_Wallet->StartTransaction(acceptParams); - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 200); - - mainReactor->run(); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.size() == 1); - WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); - WALLET_CHECK(receiverCoins[0].m_createTxId == txID); - - auto senderCoins = sender.GetCoins(); - WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - // change - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - - // TODO: check secret for "aggregate signature" scheme - // check secret - //uintBig senderSecret(Zero); - //storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::PreImage, senderSecret); - //uintBig receiverSecret(Zero); - //storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::PreImage, receiverSecret); - //WALLET_CHECK(senderSecret != Zero && senderSecret == receiverSecret); -} - -// TODO need to implement new test -void TestSwapEthRefundTransaction() -{ - cout << "\nAtomic swap: testing ETH refund transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - auto completeAction = [mainReactor](auto) - { - mainReactor->stop(); - }; - - Amount beamAmount = 300; - Amount beamFee = 101; - Amount swapAmount = 2'000'000'000u; - Amount gasPrice = 30u; - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - - ethereum::Settings aliceSettings; - aliceSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; - aliceSettings.m_accountIndex = 3; - //aliceSettings.m_address = "127.0.0.1:7545"; - aliceSettings.m_shouldConnect = true; - aliceSettings.m_lockTimeInBlocks = 20; // speed-up test - aliceSettings.m_lockTxMinConfirmations = 0; // speed-up test - //aliceSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; - - ethereum::Settings bobSettings; - bobSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; - bobSettings.m_accountIndex = 4; - //bobSettings.m_address = "127.0.0.1:7545"; - bobSettings.m_shouldConnect = true; - bobSettings.m_lockTimeInBlocks = 20; // speed-up test - bobSettings.m_lockTxMinConfirmations = 0; // speed-up test - //bobSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; - - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - - auto sender = std::make_unique(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - auto receiver = std::make_shared(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - InitEthereum(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); - InitEthereum(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - auto receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; - Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); - - auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Ethereum, swapAmount, gasPrice, false); - - receiver->m_Wallet->StartTransaction(parameters); - TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, gasPrice)); - - io::Timer::Ptr timer = io::Timer::create(*mainReactor); - timer->start(1000, true, [&node]() {node.AddBlock(); }); - - io::AsyncEvent::Ptr eventToUpdate; - //uint64_t startBlocks = receiverBtcWallet.getBlockCount(); - - eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate]() - { - if (sender) - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) - { - // delete sender to simulate refund on ETH side - sender.reset(); - } - eventToUpdate->post(); - } - else - { - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - } - }); - - eventToUpdate->post(); - mainReactor->run(); - - // validate receiver TX state - wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; - storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); - WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); - receiverCoins = receiver->GetCoins(); - WALLET_CHECK(receiverCoins.size() == 0); -} - -// TODO roman.strilets need to implement new test -void TestERC20SwapTransaction(bool isBeamOwnerStart, beam::Height fork1Height) -{ - cout << "\nTesting ERC20 atomic swap transaction...\n"; - - io::Reactor::Ptr mainReactor{ io::Reactor::create() }; - io::Reactor::Scope scope(*mainReactor); - - int completedCount = 2; - auto completeAction = [&completedCount, mainReactor](auto) - { - --completedCount; - if (completedCount == 0) - { - mainReactor->stop(); - completedCount = 2; - } - }; - - Amount beamAmount = 300; - Amount beamFee = 101; - Amount swapAmount = 1'000'000'000u; - Amount gasPrice = 30u; - - auto senderWalletDB = createSenderWalletDB(0, 0); - auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); - - ethereum::Settings aliceSettings; - aliceSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; - aliceSettings.m_accountIndex = 3; - //aliceSettings.m_address = "127.0.0.1:7545"; - aliceSettings.m_shouldConnect = true; - aliceSettings.m_lockTxMinConfirmations = 2; - /*aliceSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; - aliceSettings.m_erc20SwapContractAddress = "0x1268071E90CEE6ed135292008f010f60a542c523"; - aliceSettings.m_daiContractAddress = "0x4A2043c5625ec1E6759EA429C6FF8C02979e291E";*/ - - ethereum::Settings bobSettings; - bobSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; - bobSettings.m_accountIndex = 4; - //bobSettings.m_address = "127.0.0.1:7545"; - bobSettings.m_shouldConnect = true; - bobSettings.m_lockTxMinConfirmations = 2; - /*bobSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; - bobSettings.m_erc20SwapContractAddress = "0x1268071E90CEE6ed135292008f010f60a542c523"; - bobSettings.m_daiContractAddress = "0x4A2043c5625ec1E6759EA429C6FF8C02979e291E";*/ - - auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); - auto receiverWalletDB = createReceiverWalletDB(); - auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); - - TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); - - InitEthereum(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); - InitEthereum(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); - - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - TxID txID = { {0} }; - - auto receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.empty()); - - Node node; - - NodeObserver observer([&]() - { - auto cursor = node.get_Processor().m_Cursor; - if (cursor.m_Sid.m_Height == fork1Height + 5) - { - auto currentHeight = cursor.m_Sid.m_Height; - bool isBeamSide = !isBeamOwnerStart; - auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, - currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Dai, swapAmount, - gasPrice, isBeamSide); - - TestWalletRig* initiator = &sender; - TestWalletRig* acceptor = &receiver; - if (isBeamOwnerStart) - { - std::swap(initiator, acceptor); - } - - initiator->m_Wallet->StartTransaction(parameters); - auto acceptParams = AcceptSwapParameters(parameters, beamFee, gasPrice); - - txID = acceptor->m_Wallet->StartTransaction(acceptParams); - } - }); - - InitNodeToTest(node, binaryTreasury, &observer, 32125, 200); - - mainReactor->run(); - - { - WALLET_CHECK(senderSP->CanModify() == true); - WALLET_CHECK(receiverSP->CanModify() == true); - - receiverCoins = receiver.GetCoins(); - WALLET_CHECK(receiverCoins.size() == 1); - WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); - WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); - WALLET_CHECK(receiverCoins[0].m_createTxId == txID); - - auto senderCoins = sender.GetCoins(); - WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); - WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); - WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); - WALLET_CHECK(senderCoins[0].m_spentTxId == txID); - // change - WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); - WALLET_CHECK(senderCoins[4].m_status == Coin::Available); - WALLET_CHECK(senderCoins[4].m_createTxId == txID); - } -} - -int main() -{ - int logLevel = BEAM_LOG_LEVEL_INFO; - const auto path = boost::filesystem::system_complete("logs"); - auto logger = beam::Logger::create(logLevel, logLevel, BEAM_LOG_LEVEL_DEBUG, "swap_test", path.string()); - - Rules::get().FakePoW = true; - beam::Height fork1Height = 10; - Rules::get().pForks[1].m_Height = fork1Height; - Rules::get().pForks[2].m_Height = fork1Height; - Rules::get().DisableForksFrom(3); // swap values currently specified in the test are insufficient for fees after HF3 - Rules::get().UpdateChecksum(); - - TestSwapTransaction(true, fork1Height); - TestSwapTransaction(false, fork1Height); - TestSwapTransaction(true, fork1Height); - TestSwapTransaction(false, fork1Height); - TestSwapTransactionWithoutChange(true); - - TestSwapBTCQuickRefundTransaction(); - - TestSwapBTCRefundTransaction(); - TestSwapBeamRefundTransaction(); - //TestSwapBeamAndBTCRefundTransaction(); - //TestSwapBTCRedeemAfterExpired(); - - ExpireByResponseTime(true); - ExpireByResponseTime(false); - TestExpireByLifeTime(); - - TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::Initial); - - TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::BuildingBeamLockTX); - TestSwapCancelTransaction(false, wallet::AtomicSwapTransaction::State::BuildingBeamLockTX); - - TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::BuildingBeamRedeemTX); - TestSwapCancelTransaction(false, wallet::AtomicSwapTransaction::State::BuildingBeamRedeemTX); - - TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::BuildingBeamRefundTX); - TestSwapCancelTransaction(false, wallet::AtomicSwapTransaction::State::BuildingBeamRefundTX); - - wallet::g_EnforceTestnetSwap = true; - - TestElectrumSwapTransaction(true, fork1Height); - TestElectrumSwapTransaction(false, fork1Height); - - TestElectrumSwapBTCRefundTransaction(); - TestElectrumSwapBeamRefundTransaction(); - - TestIgnoringThirdPeer(); - - //TestEthSwapTransaction(true, fork1Height); - //TestSwapEthRefundTransaction(); - //TestERC20SwapTransaction(true, fork1Height); - - assert(g_failureCount == 0); - return WALLET_CHECK_RESULT; -} +// Copyright 2019 The Beam Team +// +// 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. + +#include "wallet/core/common.h" +#include "wallet/core/wallet_network.h" +#include "wallet/core/wallet.h" +#include "wallet/core/simple_transaction.h" +#include "keykeeper/local_private_key_keeper.h" +#include "wallet/core/secstring.h" +#include "wallet/transactions/swaps/common.h" +#include "wallet/transactions/swaps/swap_transaction.h" +#include "wallet/transactions/swaps/utils.h" +#include "wallet/transactions/swaps/second_side.h" +#include "wallet/transactions/swaps/bridges/bitcoin/bitcoin.h" +#include "wallet/transactions/swaps/bridges/ethereum/ethereum.h" + +#include "http/http_client.h" +#include "utility/test_helpers.h" +#include "core/radixtree.h" +#include "core/unittest/mini_blockchain.h" +#include "core/negotiator.h" +#include "node/node.h" +#include "utility/io/sslserver.h" + +#include "test_helpers.h" +#include "wallet_test_node.h" + +#include +#include +#include +#include +#include + +using namespace beam; +using namespace std; +using namespace ECC; + +WALLET_TEST_INIT + +#include "wallet_test_environment.cpp" +#include "swap_test_environment.cpp" + +namespace +{ + const AmountList kDefaultTestAmounts = { 500, 200, 100, 900 }; + const Height kNodeStartHeight = 145; + const uint16_t kBtcTxMinConfirmations = 2; + const uint32_t kLockTimeInBlocks = 100; + + TestBitcoinWallet GetSenderBTCWallet(io::Reactor& reactor, const io::Address& senderAddress, Amount swapAmount) + { + TestBitcoinWallet::Options senderOptions; + senderOptions.m_rawAddress = "2N8N2kr34rcGqHCo3aN6yqniid8a4Mt3FCv"; + senderOptions.m_privateKey = "cSFMca7FAeAgLRgvev5ajC1v1jzprBr1KoefUFFPS8aw3EYwLArM"; + senderOptions.m_refundTx = "0200000001809fc0890cb2724a941dfc3b7213a63b3017b0cddbed4f303be300cb55ddca830100000000ffffffff01e8030000000000001976a9146ed612a79317bc6ade234f299073b945ccb3e76b88ac00000000"; + senderOptions.m_amount = swapAmount; + + return TestBitcoinWallet(reactor, senderAddress, senderOptions); + } + + TestBitcoinWallet GetReceiverBTCWallet(io::Reactor& reactor, const io::Address& receiverAddress, Amount swapAmount) + { + TestBitcoinWallet::Options receiverOptions; + receiverOptions.m_rawAddress = "2Mvfsv3JiwWXjjwNZD6LQJD4U4zaPAhSyNB"; + receiverOptions.m_privateKey = "cNoRPsNczFw6b7wTuwLx24gSnCPyF3CbvgVmFJYKyfe63nBsGFxr"; + receiverOptions.m_refundTx = "0200000001809fc0890cb2724a941dfc3b7213a63b3017b0cddbed4f303be300cb55ddca830100000000ffffffff01e8030000000000001976a9146ed612a79317bc6ade234f299073b945ccb3e76b88ac00000000"; + receiverOptions.m_amount = swapAmount; + + return TestBitcoinWallet(reactor, receiverAddress, receiverOptions); + } + + TxParameters AcceptSwapParameters(const TxParameters& initialParameters, Amount fee, Amount feeRate) + { + TxParameters parameters = initialParameters; + + parameters.SetParameter(TxParameterID::PeerAddr, *parameters.GetParameter(TxParameterID::MyAddr)); + parameters.DeleteParameter(TxParameterID::MyAddr); + parameters.DeleteParameter(TxParameterID::MyAddressID); + + bool isBeamSide = !*parameters.GetParameter(TxParameterID::AtomicSwapIsBeamSide); + + if (isBeamSide) + { + // delete parameters from other side + parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::BEAM_REDEEM_TX); + parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::LOCK_TX); + parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::REFUND_TX); + + // add our parameters + parameters.SetParameter(TxParameterID::Fee, fee, SubTxIndex::BEAM_LOCK_TX); + parameters.SetParameter(TxParameterID::Fee, fee, SubTxIndex::BEAM_REFUND_TX); + parameters.SetParameter(TxParameterID::Fee, feeRate, SubTxIndex::REDEEM_TX); + } + else + { + // delete parameters from other side + parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::BEAM_LOCK_TX); + parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::BEAM_REFUND_TX); + parameters.DeleteParameter(TxParameterID::Fee, SubTxIndex::REDEEM_TX); + + // add our parameters + parameters.SetParameter(TxParameterID::Fee, fee, SubTxIndex::BEAM_REDEEM_TX); + parameters.SetParameter(TxParameterID::Fee, feeRate, SubTxIndex::LOCK_TX); + parameters.SetParameter(TxParameterID::Fee, feeRate, SubTxIndex::REFUND_TX); + } + + parameters.SetParameter(TxParameterID::IsSender, isBeamSide); + parameters.SetParameter(TxParameterID::AtomicSwapIsBeamSide, isBeamSide); + parameters.SetParameter(TxParameterID::IsInitiator, true); + + return parameters; + } + + class TestSettings : public bitcoin::Settings + { + public: + TestSettings() + { + SetLockTxMinConfirmations(kBtcTxMinConfirmations); + SetLockTimeInBlocks(kLockTimeInBlocks); + } + }; + + TxParameters InitNewSwap2( + const TestWalletRig& twr, Height minHeight, Amount amount, + Amount fee, AtomicSwapCoin swapCoin, Amount swapAmount, Amount swapFee, + bool isBeamSide = true, Height lifetime = kDefaultTxLifetime, + Height responseTime = kDefaultTxResponseTime) + { + return InitNewSwap(*twr.m_WalletDB, minHeight, amount, fee, swapCoin, swapAmount, swapFee, isBeamSide, lifetime, responseTime); + } + + // Treasury with BEAM coins for two wallets plus confidential-asset coins + // for the first one. Requires CA active from genesis (fork2 at height 0), + // since treasury outputs are validated at height 0. + ByteBuffer createAssetTreasury( + IWalletDB::Ptr senderDB, const AmountList& senderAmounts, + IWalletDB::Ptr receiverDB, const AmountList& receiverAmounts, + Asset::ID assetID, const AmountList& assetAmounts) + { + Treasury treasury; + + std::vector> plans; + if (!senderAmounts.empty()) + plans.emplace_back(senderDB, &senderAmounts); + if (!receiverAmounts.empty()) + plans.emplace_back(receiverDB, &receiverAmounts); + + for (const auto& [db, pAmounts] : plans) + { + const AmountList& amounts = *pAmounts; + + PeerID pid; + ECC::Scalar::Native sk; + Treasury::get_ID(*db->get_MasterKdf(), pid, sk); + + Treasury::Parameters params; + params.m_Bursts = 1U; + params.m_MaturityStep = 1; + + Treasury::Entry* plan = treasury.CreatePlan(pid, 0, params); + beam::Height incubation = 0; + for (size_t i = 0; i < amounts.size(); ++i) + { + if (i == 0) + { + plan->m_Request.m_vGroups.front().m_vCoins.front().m_Value = amounts[i]; + incubation = plan->m_Request.m_vGroups.front().m_vCoins.front().m_Incubation; + continue; + } + + auto& c = plan->m_Request.m_vGroups.back().m_vCoins.emplace_back(); + c.m_Incubation = incubation; + c.m_Value = amounts[i]; + } + + plan->m_pResponse.reset(new Treasury::Response); + uint64_t nIndex = 1; + plan->m_pResponse->Create(plan->m_Request, *db->get_MasterKdf(), nIndex); + + for (const auto& group : plan->m_pResponse->m_vGroups) + { + for (const auto& treasuryCoin : group.m_vCoins) + { + CoinID cid; + if (treasuryCoin.m_pOutput->Recover(0, *db->get_MasterKdf(), cid)) + { + Coin coin; + coin.m_ID = cid; + coin.m_maturity = treasuryCoin.m_pOutput->m_Incubation; + coin.m_confirmHeight = treasuryCoin.m_pOutput->m_Incubation; + db->saveCoin(coin); + } + } + } + } + + Treasury::Data data; + data.m_sCustomMsg = "LN"; + treasury.Build(data); + + if (assetID && !assetAmounts.empty()) + { + // extra group of asset UTXOs, balanced against Group::m_Value on + // the asset generator (mirrors Treasury::Response::Group::Create) + Treasury::Data::Group g; + g.m_Aid = assetID; + ZeroObject(g.m_Value); + g.m_Data.m_Offset = Zero; + + Key::IKdf& kdf = *senderDB->get_MasterKdf(); + ECC::Scalar::Native sk, offset = Zero; + uint64_t nIndex = 0x220000; + + for (Amount v : assetAmounts) + { + CoinID cid(Zero); + cid.m_Idx = nIndex++; + cid.m_Type = Key::Type::Treasury; + cid.set_Subkey(0, CoinID::Scheme::V1); + cid.m_Value = v; + cid.m_AssetID = assetID; + + Output::Ptr pOutp = std::make_unique(); + pOutp->m_Incubation = 1; + pOutp->Create(0, sk, kdf, cid, kdf); + offset += sk; + + Coin coin; + coin.m_ID = cid; + coin.m_maturity = pOutp->m_Incubation; + coin.m_confirmHeight = pOutp->m_Incubation; + senderDB->saveCoin(coin); + + g.m_Data.m_vOutputs.push_back(std::move(pOutp)); + g.m_Value += MultiWord::From(v); + } + + kdf.DeriveKey(sk, Key::ID(nIndex++, FOURCC_FROM(KeR3))); + TxKernelStd::Ptr pKrn = std::make_unique(); + pKrn->Sign(sk); + g.m_Data.m_vKernels.push_back(std::move(pKrn)); + offset += sk; + + offset = -offset; + g.m_Data.m_Offset = offset; + g.m_Data.Normalize(); + + data.m_vGroups.push_back(std::move(g)); + } + + Serializer ser; + ser & data; + + ByteBuffer result; + ser.swap_buf(result); + + return result; + } + +} + +bitcoin::ISettingsProvider::Ptr InitSettingsProvider(IWalletDB::Ptr walletDB, const TestSettings& settings) +{ + auto settingsProvider = std::make_shared(walletDB); + settingsProvider->SetSettings(settings); + return settingsProvider; +} + +void InitBitcoin(Wallet& wallet, IWalletDB::Ptr walletDB, io::Reactor& reactor, bitcoin::ISettingsProvider& settingsProvider) +{ + auto creator = std::make_shared(walletDB); + auto bridge = std::make_shared(reactor, settingsProvider); + // TODO should refactored this code + auto bitcoinBridgeCreator = [bridge]() -> bitcoin::IBridge::Ptr + { + return bridge; + }; + auto factory = wallet::MakeSecondSideFactory(bitcoinBridgeCreator, settingsProvider); + creator->RegisterFactory(AtomicSwapCoin::Bitcoin, factory); + wallet.RegisterTransactionType(TxType::AtomicSwap, std::static_pointer_cast(creator)); +} + +void InitElectrum(Wallet& wallet, IWalletDB::Ptr walletDB, io::Reactor& reactor, bitcoin::ISettingsProvider& settingsProvider) +{ + auto creator = std::make_shared(walletDB); + auto bridge = std::make_shared(reactor, settingsProvider); + // TODO should refactored this code + auto bitcoinBridgeCreator = [bridge]() -> bitcoin::IBridge::Ptr + { + return bridge; + }; + auto factory = wallet::MakeSecondSideFactory(bitcoinBridgeCreator, settingsProvider); + creator->RegisterFactory(AtomicSwapCoin::Bitcoin, factory); + wallet.RegisterTransactionType(TxType::AtomicSwap, std::static_pointer_cast(creator)); +} + +void TestSwapTransaction(Rules& rules, bool isBeamOwnerStart, beam::Height fork1Height) +{ + cout << "\nTesting atomic swap transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 101; + Amount swapAmount = 2000; + Amount feeRate = 256; + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + + TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + receiverBtcWallet.addPeer(senderAddress); + + TxID txID = { {0} }; + + auto receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + Node node; + + NodeObserver observer([&]() + { + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == fork1Height + 5) + { + auto currentHeight = cursor.m_hh.m_Height; + bool isBeamSide = !isBeamOwnerStart; + auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, + currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, isBeamSide); + + TestWalletRig* initiator = &sender; + TestWalletRig* acceptor = &receiver; + if (isBeamOwnerStart) + { + std::swap(initiator, acceptor); + } + + initiator->m_Wallet->StartTransaction(parameters); + auto acceptParams = AcceptSwapParameters(parameters, beamFee, feeRate); + + txID = acceptor->m_Wallet->StartTransaction(acceptParams); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 200); + + mainReactor->run(); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.size() == 1); + WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); + WALLET_CHECK(receiverCoins[0].m_createTxId == txID); + + auto senderCoins = sender.GetCoins(); + WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + // change + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + + // check secret + NoLeak senderSecretPrivateKey; + storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, senderSecretPrivateKey.V); + NoLeak receiverSecretPrivateKey; + storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, receiverSecretPrivateKey.V); + WALLET_CHECK(senderSecretPrivateKey.V != Zero && senderSecretPrivateKey.V == receiverSecretPrivateKey.V); +} + +void TestSwapAssetTransaction(bool isBeamOwnerStart) +{ + cout << "\nTesting atomic swap transaction with confidential asset...\n"; + + // CA must be active from genesis: the asset treasury outputs are + // validated at height 0 + Rules rules; + Rules::Scope scopeRules(rules); + rules.m_Consensus = Rules::Consensus::FakePoW; + rules.pForks[1].m_Height = 0; + rules.pForks[2].m_Height = 0; + rules.DisableForksFrom(3); + rules.CA.Enabled = true; + rules.UpdateChecksum(); + + bool assetsEnabledPrev = std::exchange(wallet::g_AssetsEnabled, true); + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + const Asset::ID kAssetID = 12; + const AmountList kAssetAmounts = { 500, 200 }; + const AmountList kReceiverBeamAmounts = { 500 }; + + Amount assetAmount = 300; + Amount beamFee = 101; + Amount swapAmount = 2000; + Amount feeRate = 256; + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto receiverWalletDB = createReceiverWalletDB(); + auto binaryTreasury = createAssetTreasury( + senderWalletDB, kDefaultTestAmounts, + receiverWalletDB, kReceiverBeamAmounts, + kAssetID, kAssetAmounts); + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + + TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); + + receiverBtcWallet.addPeer(senderAddress); + + TxID txID = { {0} }; + + Node node; + + NodeObserver observer([&]() + { + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == 15) + { + auto currentHeight = cursor.m_hh.m_Height; + bool isBeamSide = !isBeamOwnerStart; + auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, + currentHeight, assetAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, isBeamSide); + parameters.SetParameter(TxParameterID::AtomicSwapBeamAssetID, kAssetID); + parameters.SetParameter(TxParameterID::AtomicSwapBeamAssetName, std::string("TEST")); + + TestWalletRig* initiator = &sender; + TestWalletRig* acceptor = &receiver; + if (isBeamOwnerStart) + { + std::swap(initiator, acceptor); + } + + initiator->m_Wallet->StartTransaction(parameters); + auto acceptParams = AcceptSwapParameters(parameters, beamFee, feeRate); + + txID = acceptor->m_Wallet->StartTransaction(acceptParams); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 200); + + mainReactor->run(); + + wallet::AtomicSwapTransaction::State senderState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender.m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, senderState); + WALLET_CHECK(senderState == wallet::AtomicSwapTransaction::State::CompleteSwap); + + wallet::AtomicSwapTransaction::State receiverState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver.m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, receiverState); + WALLET_CHECK(receiverState == wallet::AtomicSwapTransaction::State::CompleteSwap); + + // the receiver gets the full asset amount: the redeem fee is paid from + // the receiver's own BEAM coins, never from the asset value + Amount receiverAsset = 0, receiverBeam = 0; + for (const auto& c : receiver.GetCoins()) + { + if (c.m_status != Coin::Available) + continue; + if (c.m_ID.m_AssetID == kAssetID) + { + receiverAsset += c.m_ID.m_Value; + WALLET_CHECK(c.m_createTxId == txID); + } + else if (!c.m_ID.m_AssetID) + receiverBeam += c.m_ID.m_Value; + } + WALLET_CHECK(receiverAsset == assetAmount); + WALLET_CHECK(receiverBeam == kReceiverBeamAmounts[0] - beamFee); + + Amount senderAsset = 0, senderBeam = 0; + for (const auto& c : sender.GetCoins()) + { + if (c.m_status != Coin::Available) + continue; + if (c.m_ID.m_AssetID == kAssetID) + senderAsset += c.m_ID.m_Value; + else if (!c.m_ID.m_AssetID) + senderBeam += c.m_ID.m_Value; + } + Amount assetTotal = std::accumulate(kAssetAmounts.begin(), kAssetAmounts.end(), Amount(0)); + Amount beamTotal = std::accumulate(kDefaultTestAmounts.begin(), kDefaultTestAmounts.end(), Amount(0)); + WALLET_CHECK(senderAsset == assetTotal - assetAmount); + WALLET_CHECK(senderBeam == beamTotal - beamFee); + + // check secret + NoLeak senderSecretPrivateKey; + storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, senderSecretPrivateKey.V); + NoLeak receiverSecretPrivateKey; + storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, receiverSecretPrivateKey.V); + WALLET_CHECK(senderSecretPrivateKey.V != Zero && senderSecretPrivateKey.V == receiverSecretPrivateKey.V); + + wallet::g_AssetsEnabled = assetsEnabledPrev; +} + +void TestElectrumSwapTransaction(Rules& rules, bool isBeamOwnerStart, beam::Height fork1Height) +{ + cout << "\nTesting atomic swap transaction on electrum...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + std::string address("127.0.0.1:10400"); + + Amount beamAmount = 300; + Amount beamFee = 102; + Amount swapAmount = 200000; + Amount feeRate = 80000; + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + + TestSettings bobSettings; + bobSettings.SetElectrumConnectionOptions({ address, {"unveil", "shadow", "gold", "piece", "salad", "parent", "leisure", "obtain", "wave", "eternal", "suggest", "artwork"}, false}); + + TestSettings aliceSettings; + aliceSettings.SetElectrumConnectionOptions({ address, {"rib", "genuine", "fury", "advance", "train", "capable", "rough", "silk", "march", "vague", "notice", "sphere"}, false}); + + TestElectrumWallet btcWallet(*mainReactor, address); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitElectrum(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); + InitElectrum(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); + + TxID txID = { {0} }; + + auto receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + Node node; + + NodeObserver observer([&]() + { + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == fork1Height + 5) + { + auto currentHeight = cursor.m_hh.m_Height; + bool isBeamSide = !isBeamOwnerStart; + auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, + currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, isBeamSide); + + if (isBeamOwnerStart) + { + receiver.m_Wallet->StartTransaction(parameters); + txID = sender.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + } + else + { + sender.m_Wallet->StartTransaction(parameters); + txID = receiver.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + } + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 200); + + mainReactor->run(); + + receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.size() == 1); + WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); + WALLET_CHECK(receiverCoins[0].m_createTxId == txID); + + auto senderCoins = sender.GetCoins(); + WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + // change + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + + // check secret + NoLeak senderSecretPrivateKey; + storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, senderSecretPrivateKey.V); + NoLeak receiverSecretPrivateKey; + storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::AtomicSwapSecretPrivateKey, receiverSecretPrivateKey.V); + WALLET_CHECK(senderSecretPrivateKey.V != Zero && senderSecretPrivateKey.V == receiverSecretPrivateKey.V); +} + +void TestSwapTransactionWithoutChange(bool isBeamOwnerStart) +{ + cout << "\nTesting atomic swap transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 380; + Amount beamFee = 120; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); + + receiverBtcWallet.addPeer(senderAddress); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + TxID txID = { {0} }; + + if (isBeamOwnerStart) + { + auto parameters = InitNewSwap2(receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver.m_Wallet->StartTransaction(parameters); + txID = sender.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + } + else + { + auto parameters = InitNewSwap2(sender, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, true); + sender.m_Wallet->StartTransaction(parameters); + txID = receiver.m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + } + + auto receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + mainReactor->run(); + + receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.size() == 1); + WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); + WALLET_CHECK(receiverCoins[0].m_createTxId == txID); + + auto senderCoins = sender.GetCoins(); + WALLET_CHECK(senderCoins.size() == 4); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); +} + +void TestSwapBTCRefundTransaction() +{ + cout << "\nAtomic swap: testing BTC refund transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + io::AsyncEvent::Ptr eventToUpdate; + uint64_t startBlocks = receiverBtcWallet.getBlockCount(); + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate, &timer]() + { + if (sender) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) + { + // delete sender to simulate refund on BTC side + sender.reset(); + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState != wallet::AtomicSwapTransaction::State::SendingRefundTX) + { + // speed-up test + timer->restart(50, true); + } + } + }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate receiver TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.size() == 0); + WALLET_CHECK(receiverBtcWallet.getBlockCount() - startBlocks >= kLockTimeInBlocks); + + // TODO: add check BTC balance +} + +void TestSwapBTCQuickRefundTransaction() +{ + cout << "\nAtomic swap: testing BTC quick refund transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, Wallet::TxCompletedAction(), TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + io::AsyncEvent::Ptr eventToUpdate; + bool isCanceled = false; + uint64_t startBlocks = receiverBtcWallet.getBlockCount(); + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate, &timer, &isCanceled]() + { + if (!isCanceled) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) + { + sender->m_Wallet->CancelTransaction(txID); + isCanceled = true; + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState != wallet::AtomicSwapTransaction::State::SendingRefundTX) + { + // speed-up test + timer->restart(50, true); + } + } + }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate receiver TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.size() == 0); + WALLET_CHECK(receiverBtcWallet.getBlockCount() - startBlocks < kLockTimeInBlocks); +} + +void TestElectrumSwapBTCRefundTransaction() +{ + cout << "\nAtomic swap: testing BTC refund transaction on electrum...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + std::string address("127.0.0.1:10400"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 200000; + Amount feeRate = 80000; + + TestSettings bobSettings; + bobSettings.SetElectrumConnectionOptions({ address, {"unveil", "shadow", "gold", "piece", "salad", "parent", "leisure", "obtain", "wave", "eternal", "suggest", "artwork"}, false}); + + TestSettings aliceSettings; + aliceSettings.SetElectrumConnectionOptions({ address, {"rib", "genuine", "fury", "advance", "train", "capable", "rough", "silk", "march", "vague", "notice", "sphere"}, false}); + + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + TestElectrumWallet btcWallet(*mainReactor, address); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitElectrum(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitElectrum(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + io::AsyncEvent::Ptr eventToUpdate; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate, &timer]() + { + if (sender) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) + { + // delete sender to simulate refund on BTC side + sender.reset(); + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState != wallet::AtomicSwapTransaction::State::SendingRefundTX) + { + // speed-up test + timer->restart(50, true); + } + } + }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate receiver TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.size() == 0); +} + +void TestSwapBeamRefundTransaction() +{ + cout << "\nAtomic swap: testing Beam refund transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 350; + Amount beamFee = 125; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + cout << "receiver tx start" << endl; + receiver->m_Wallet->StartTransaction(parameters); + cout << "sender tx start" << endl; + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + cout << "waiting..." << endl; + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::AsyncEvent::Ptr eventToUpdate; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, &receiver, txID, &eventToUpdate, &node]() + { + if (receiver) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) + { + cout << "deleting rcvr" << endl; + + // delete receiver to simulate refund on Beam side + receiver.reset(); + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState != wallet::AtomicSwapTransaction::State::SendingBeamRefundTX) + { + // speed-up test + node.AddBlock(); + eventToUpdate->post(); + } + } + }); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == 6); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + + // change of Beam LockTx + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + + // Refund + WALLET_CHECK(senderCoins[5].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(senderCoins[5].m_status == Coin::Available); + WALLET_CHECK(senderCoins[5].m_createTxId == txID); +} + +void TestSwapAssetRefundTransaction() +{ + cout << "\nAtomic swap: testing Beam confidential-asset refund transaction...\n"; + + bool assetsEnabledPrev = std::exchange(wallet::g_AssetsEnabled, true); + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + const Asset::ID kAssetID = 12; + const AmountList kAssetAmounts = { 500, 200 }; + + Amount assetAmount = 350; + Amount beamFee = 125; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + for (Amount v : kAssetAmounts) + { + Coin coin = CreateAvailCoin(v, 0); + coin.m_ID.m_AssetID = kAssetID; + senderWalletDB->storeCoin(coin); + } + + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + { + // the redeem fee is paid from the receiver's own BEAM coins + Coin coin = CreateAvailCoin(500, 0); + receiverWalletDB->storeCoin(coin); + } + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + auto parameters = InitNewSwap2(*receiver, currentHeight, assetAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + parameters.SetParameter(TxParameterID::AtomicSwapBeamAssetID, kAssetID); + parameters.SetParameter(TxParameterID::AtomicSwapBeamAssetName, std::string("TEST")); + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + + io::AsyncEvent::Ptr eventToUpdate; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, &receiver, txID, &eventToUpdate, &node]() + { + if (receiver) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) + { + // delete receiver to simulate refund on Beam side + receiver.reset(); + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState != wallet::AtomicSwapTransaction::State::SendingBeamRefundTX) + { + // speed-up test + node.AddBlock(); + eventToUpdate->post(); + } + } + }); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + + // the sender gets the FULL asset amount back: the refund fee was paid + // from the sender's own BEAM coins, never from the asset value + Amount senderAsset = 0, senderBeam = 0; + bool foundRefundCoin = false; + for (const auto& c : sender->GetCoins()) + { + if (c.m_status != Coin::Available) + continue; + if (c.m_ID.m_AssetID == kAssetID) + { + senderAsset += c.m_ID.m_Value; + if (c.m_createTxId == txID && c.m_ID.m_Value == assetAmount) + foundRefundCoin = true; + } + else if (!c.m_ID.m_AssetID) + senderBeam += c.m_ID.m_Value; + } + WALLET_CHECK(foundRefundCoin); + + Amount assetTotal = std::accumulate(kAssetAmounts.begin(), kAssetAmounts.end(), Amount(0)); + Amount beamTotal = std::accumulate(kDefaultTestAmounts.begin(), kDefaultTestAmounts.end(), Amount(0)); + WALLET_CHECK(senderAsset == assetTotal); + WALLET_CHECK(senderBeam == beamTotal - 2 * beamFee); // lock fee + refund fee + + wallet::g_AssetsEnabled = assetsEnabledPrev; +} + +void TestSwapBeamAndBTCRefundTransaction(Rules& rules) +{ + cout << "\nAtomic swap: testing Beam and BTC refund transactions...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completedAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 350; + Amount beamFee = 125; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + vector receiverCoins; + io::AsyncEvent::Ptr eventToUpdate; + bool isNeedReset = true, bTxStarted = false; + Node node; + TxID txID; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() + { + eventToUpdate->post(); + + if (!bTxStarted) + return; + + if (receiver && isNeedReset) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) + { + cout << "Deleting receiver" << endl; + + // delete receiver to simulate refund on Beam side + receiver.reset(); + isNeedReset = false; + node.m_Cfg.m_TestMode.m_FakePowSolveTime_ms = 500; + } + } + else + { + Height minHeight; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::MinHeight, minHeight); + Height currentHeight = sender->m_WalletDB->getCurrentHeight(); + + if (currentHeight - minHeight > 5 * 60 && !receiver) + { + cout << "Restoring receiver" << endl; + + receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + receiver->m_Wallet->ResumeAllTransactions(); + } + } + }); + + + NodeObserver observer([&]() + { + Height minHeight = 15; + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == minHeight) + { + cout << "Starting tx" << endl; + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + auto parameters = InitNewSwap2(*receiver, minHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + bTxStarted = true; + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 500); + + + eventToUpdate->post(); + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == 6); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + + // change of Beam LockTx + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + + // Refund + WALLET_CHECK(senderCoins[5].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(senderCoins[5].m_status == Coin::Available); + WALLET_CHECK(senderCoins[5].m_createTxId == txID); + + txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); +} + +void TestSwapBTCRedeemAfterExpired(Rules& rules) +{ + cout << "\nAtomic swap: testing BTC redeem after Beam expired...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completedAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 350; + Amount beamFee = 125; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + vector receiverCoins; + io::AsyncEvent::Ptr eventToUpdate; + bool isNeedReset = true; + Node node; + TxID txID; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() + { + if (sender && isNeedReset) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) + { + sender.reset(); + isNeedReset = false; + node.m_Cfg.m_TestMode.m_FakePowSolveTime_ms = 500; + } + } + else + { + Height minHeight; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::MinHeight, minHeight); + Height currentHeight = receiver->m_WalletDB->getCurrentHeight(); + + if (currentHeight - minHeight > 6 * 60 && !sender) + { + sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + sender->m_Wallet->ResumeAllTransactions(); + } + + if (currentHeight - minHeight > 500) + { + mainReactor->stop(); + } + } + eventToUpdate->post(); + }); + + + NodeObserver observer([&]() + { + Height minHeight = 15; + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == minHeight) + { + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + auto parameters = InitNewSwap2(*receiver, minHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 500); + + + eventToUpdate->post(); + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::CompleteSwap); + + txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::CompleteSwap); +} + +void TestElectrumSwapBeamRefundTransaction() +{ + cout << "\nAtomic swap: testing Beam refund transaction on electrum...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + std::string address("127.0.0.1:10400"); + + Amount beamAmount = 320; + Amount beamFee = 110; + Amount swapAmount = 200000; + Amount feeRate = 80000; + + TestSettings bobSettings; + bobSettings.SetElectrumConnectionOptions({ address, {"unveil", "shadow", "gold", "piece", "salad", "parent", "leisure", "obtain", "wave", "eternal", "suggest", "artwork"}, false}); + + TestSettings aliceSettings; + aliceSettings.SetElectrumConnectionOptions({ address, {"rib", "genuine", "fury", "advance", "train", "capable", "rough", "silk", "march", "vague", "notice", "sphere"}, false}); + + TestElectrumWallet btcWallet(*mainReactor, address); + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_unique(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitElectrum(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitElectrum(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::AsyncEvent::Ptr eventToUpdate; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, &receiver, txID, &eventToUpdate, &node]() + { + if (receiver) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::SendingBeamRedeemTX) + { + // delete receiver to simulate refund on Beam side + receiver.reset(); + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState != wallet::AtomicSwapTransaction::State::SendingBeamRefundTX) + { + // speed-up test + node.AddBlock(); + eventToUpdate->post(); + } + } + }); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == 6); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + + // change of Beam LockTx + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + + // Refund + WALLET_CHECK(senderCoins[5].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(senderCoins[5].m_status == Coin::Available); + WALLET_CHECK(senderCoins[5].m_createTxId == txID); +} + +void ExpireByResponseTime(bool isBeamSide) +{ + // Simulate swap transaction without response from second side + + cout << "\nAtomic swap: testing expired transaction on " << (isBeamSide ? "Beam" : "BTC") << " side...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 2000; + Amount feeRate = 256; + Height lifetime = 100; + Height responseTime = 100; + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", senderAddress }); + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, completedAction); + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + + auto db = createReceiverWalletDB(); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + auto swapParameters = InitNewSwap(*db, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, !isBeamSide, lifetime, responseTime); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(swapParameters, beamFee, feeRate)); + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(50, true, [&node]() {node.AddBlock(); }); + + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Failed); + + TxFailureReason reason = TxFailureReason::Unknown; + storage::getTxParameter(*sender->m_WalletDB, txID, TxParameterID::InternalFailureReason, reason); + WALLET_CHECK(reason == TxFailureReason::TransactionExpired); + + if (isBeamSide) + { + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == 4); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Available); + } +} + +void TestSwapCancelTransaction(bool isSender, wallet::AtomicSwapTransaction::State testingState) +{ + cout << "\nAtomic swap: testing cancel transaction (" << (isSender ? "sender" : "receiver") << ", " << wallet::getSwapTxStatus(testingState) << ")...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completedAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, isSender ? Wallet::TxCompletedAction() : completedAction); + auto receiver = std::make_unique(receiverWalletDB, isSender ? completedAction : Wallet::TxCompletedAction()); + + receiverBtcWallet.addPeer(senderAddress); + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + TxID txID = receiver->m_Wallet->StartTransaction(parameters); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + bool bStarted = false; + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(1000, true, [&node, &bStarted, &sender, &receiver, ¶meters, beamFee, feeRate]() + { + node.AddBlock(); + + if (!bStarted && receiver->m_Wallet->IsWalletInSync() && sender->m_Wallet->IsWalletInSync()) + { + sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + bStarted = true; + io::Reactor::get_Current().stop(); + } + }); + + mainReactor->run(); + WALLET_CHECK(bStarted); + + + io::AsyncEvent::Ptr eventToUpdate; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&walletRig = isSender ? sender: receiver, testingState, txID, &eventToUpdate]() + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*walletRig->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == testingState) + { + walletRig->m_Wallet->CancelTransaction(txID); + } + else + { + eventToUpdate->post(); + } + }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate sender TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == (isSender ? wallet::AtomicSwapTransaction::State::Canceled : wallet::AtomicSwapTransaction::State::Failed)); + + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == (isSender ? wallet::AtomicSwapTransaction::State::Failed : wallet::AtomicSwapTransaction::State::Canceled)); + + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == 4); + + for (const auto& coin : senderCoins) + { + WALLET_CHECK(coin.m_status == Coin::Available); + } +} + +void TestExpireByLifeTime(Rules& rules) +{ + cout << "\nAtomic swap: expire by lifetime ...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + auto receiverWalletDB = createReceiverWalletDB(); + auto senderWalletDB = createSenderWalletDB(0, 0); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + auto sender = std::make_unique(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_unique(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + receiverBtcWallet.addPeer(senderAddress); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + bool isNeedReset = true; + io::AsyncEvent::Ptr eventToUpdate; + Node node; + TxID txID; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() + { + if (receiver) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::Failed) + { + return; + } + if (txState == wallet::AtomicSwapTransaction::State::BuildingBeamLockTX && isNeedReset) + { + receiver.reset(); + isNeedReset = false; + } + } + else if (sender) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::Failed) + { + receiver = std::make_unique(receiverWalletDB, completeAction); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + receiver->m_Wallet->ResumeAllTransactions(); + } + } + eventToUpdate->post(); + }); + + eventToUpdate->post(); + + NodeObserver observer([&]() + { + Height minHeight = 5; + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == minHeight) + { + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + auto parameters = InitNewSwap2(*receiver, minHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 200); + + mainReactor->run(); + + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Failed); + + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Failed); + + TxFailureReason reason = TxFailureReason::Unknown; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, TxParameterID::InternalFailureReason, reason); + WALLET_CHECK(reason == TxFailureReason::TransactionExpired); + + reason = TxFailureReason::Unknown; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, TxParameterID::InternalFailureReason, reason); + WALLET_CHECK(reason == TxFailureReason::TransactionExpired); + + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size()); + + for (size_t i = 0; i < kDefaultTestAmounts.size(); i++) + { + WALLET_CHECK(senderCoins[i].m_status == Coin::Available); + WALLET_CHECK(senderCoins[i].m_ID.m_Value == kDefaultTestAmounts[i]); + } +} + +void TestIgnoringThirdPeer() +{ + cout << "\nAtomic swap: testing ignoring of third peer\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completedAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 100; + Amount swapAmount = 2000; + Amount feeRate = 256; + + TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress }); + + TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount); + receiverBtcWallet.addPeer(senderAddress); + + auto senderWalletDB = createSenderWalletDB(false, kDefaultTestAmounts); + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + auto sender = std::make_unique(senderWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_shared(receiverWalletDB, completedAction, TestWalletRig::RegularWithoutPoWBbs); + + InitBitcoin(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitBitcoin(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Bitcoin, swapAmount, feeRate, false); + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, feeRate)); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(200, true, [&node]() {node.AddBlock(); }); + + io::AsyncEvent::Ptr eventToUpdate; + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&]() + { + WalletID peerID; + bool result = storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::PeerAddr, peerID); + if (result) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::BuildingBeamLockTX) + { + // create new address + WalletAddress newAddress; + sender->m_WalletDB->createAddress(newAddress); + sender->m_WalletDB->saveAddress(newAddress); + + // send msg from new address + SetTxParameter msg; + msg.AddParameter(TxParameterID::SubTxIndex, SubTxIndex::BEAM_REFUND_TX) + .AddParameter(TxParameterID::PeerSignature, ECC::Scalar::Native()); + + msg.m_TxID = txID; + msg.m_Type = wallet::TxType::AtomicSwap; + msg.m_From = newAddress.m_BbsAddr; + + sender->m_messageEndpoint->Send(receiver->m_BbsAddr, msg); + return; + } + } + eventToUpdate->post(); + }); + + eventToUpdate->post(); + mainReactor->run(); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.size() == 1); + WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); + WALLET_CHECK(receiverCoins[0].m_createTxId == txID); + + auto senderCoins = sender->GetCoins(); + WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + // change + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); +} + +namespace beam::ethereum +{ + class Provider : public ISettingsProvider + { + public: + Provider(const Settings& settings) + : m_settings(settings) + { + } + + Settings GetSettings() const override + { + return m_settings; + } + + void SetSettings(const Settings& settings) override + { + m_settings = settings; + } + + bool CanModify() const override + { + return true; + } + + void AddRef() override + { + } + + void ReleaseRef() override + { + + } + + private: + Settings m_settings; + }; +} + +ethereum::ISettingsProvider::Ptr InitSettingsProvider(IWalletDB::Ptr walletDB, const ethereum::Settings& settings) +{ + auto settingsProvider = std::make_shared(walletDB); + settingsProvider->SetSettings(settings); + return settingsProvider; +} + +void InitEthereum(Wallet& wallet, IWalletDB::Ptr walletDB, io::Reactor& reactor, ethereum::ISettingsProvider& settingsProvider) +{ + auto creator = std::make_shared(walletDB); + auto bridge = std::make_shared(reactor, settingsProvider); + // TODO should refactored this code + auto bridgeCreator = [bridge]() -> ethereum::IBridge::Ptr + { + return bridge; + }; + auto factory = wallet::MakeSecondSideFactory(bridgeCreator, settingsProvider); + creator->RegisterFactory(AtomicSwapCoin::Ethereum, factory); + creator->RegisterFactory(AtomicSwapCoin::Dai, factory); + wallet.RegisterTransactionType(TxType::AtomicSwap, std::static_pointer_cast(creator)); +} + +// TODO roman.strilets need to implement new test +void TestEthSwapTransaction(Rules& rules, bool isBeamOwnerStart, beam::Height fork1Height) +{ + cout << "\nTesting ethereum atomic swap transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + io::Address senderAddress; + senderAddress.resolve("127.0.0.1:10400"); + + io::Address receiverAddress; + receiverAddress.resolve("127.0.0.1:10300"); + + Amount beamAmount = 300; + Amount beamFee = 101; + Amount swapAmount = 2'000'000'000u; + Amount gasPrice = 30u; + //Amount feeRate = 256; + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + + ethereum::Settings aliceSettings; + aliceSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; + aliceSettings.m_accountIndex = 6; + //aliceSettings.m_address = "127.0.0.1:7545"; + aliceSettings.m_shouldConnect = true; + aliceSettings.m_lockTxMinConfirmations = 2; + //aliceSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; + + ethereum::Settings bobSettings; + bobSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; + bobSettings.m_accountIndex = 5; + //bobSettings.m_address = "127.0.0.1:7545"; + bobSettings.m_shouldConnect = true; + bobSettings.m_lockTxMinConfirmations = 2; + //bobSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; + + /*TestSettings bobSettings; + bobSettings.SetConnectionOptions({ "Bob", "123", senderAddress }); + + TestSettings aliceSettings; + aliceSettings.SetConnectionOptions({ "Alice", "123", receiverAddress });*/ + + /*TestBitcoinWallet senderBtcWallet = GetSenderBTCWallet(*mainReactor, senderAddress, swapAmount); + TestBitcoinWallet receiverBtcWallet = GetReceiverBTCWallet(*mainReactor, receiverAddress, swapAmount);*/ + + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + + TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitEthereum(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); + InitEthereum(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + //receiverBtcWallet.addPeer(senderAddress); + + TxID txID = { {0} }; + + auto receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + Node node; + + NodeObserver observer([&]() + { + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == fork1Height + 5) + { + auto currentHeight = cursor.m_hh.m_Height; + bool isBeamSide = !isBeamOwnerStart; + auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, + currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Ethereum, swapAmount, + gasPrice, isBeamSide); + + TestWalletRig* initiator = &sender; + TestWalletRig* acceptor = &receiver; + if (isBeamOwnerStart) + { + std::swap(initiator, acceptor); + } + + initiator->m_Wallet->StartTransaction(parameters); + auto acceptParams = AcceptSwapParameters(parameters, beamFee, gasPrice); + + txID = acceptor->m_Wallet->StartTransaction(acceptParams); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 200); + + mainReactor->run(); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.size() == 1); + WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); + WALLET_CHECK(receiverCoins[0].m_createTxId == txID); + + auto senderCoins = sender.GetCoins(); + WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + // change + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + + // TODO: check secret for "aggregate signature" scheme + // check secret + //uintBig senderSecret(Zero); + //storage::getTxParameter(*sender.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::PreImage, senderSecret); + //uintBig receiverSecret(Zero); + //storage::getTxParameter(*receiver.m_WalletDB, txID, SubTxIndex::BEAM_REDEEM_TX, TxParameterID::PreImage, receiverSecret); + //WALLET_CHECK(senderSecret != Zero && senderSecret == receiverSecret); +} + +// TODO need to implement new test +void TestSwapEthRefundTransaction() +{ + cout << "\nAtomic swap: testing ETH refund transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + auto completeAction = [mainReactor](auto) + { + mainReactor->stop(); + }; + + Amount beamAmount = 300; + Amount beamFee = 101; + Amount swapAmount = 2'000'000'000u; + Amount gasPrice = 30u; + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + + ethereum::Settings aliceSettings; + aliceSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; + aliceSettings.m_accountIndex = 3; + //aliceSettings.m_address = "127.0.0.1:7545"; + aliceSettings.m_shouldConnect = true; + aliceSettings.m_lockTimeInBlocks = 20; // speed-up test + aliceSettings.m_lockTxMinConfirmations = 0; // speed-up test + //aliceSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; + + ethereum::Settings bobSettings; + bobSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; + bobSettings.m_accountIndex = 4; + //bobSettings.m_address = "127.0.0.1:7545"; + bobSettings.m_shouldConnect = true; + bobSettings.m_lockTimeInBlocks = 20; // speed-up test + bobSettings.m_lockTxMinConfirmations = 0; // speed-up test + //bobSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; + + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + + auto sender = std::make_unique(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + auto receiver = std::make_shared(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitEthereum(*sender->m_Wallet, sender->m_WalletDB, *mainReactor, *senderSP); + InitEthereum(*receiver->m_Wallet, receiver->m_WalletDB, *mainReactor, *receiverSP); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + auto receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + TestNode node{ TestNode::NewBlockFunc(), kNodeStartHeight }; + Height currentHeight = node.m_Blockchain.m_mcm.m_vStates.size(); + + auto parameters = InitNewSwap2(*receiver, currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Ethereum, swapAmount, gasPrice, false); + + receiver->m_Wallet->StartTransaction(parameters); + TxID txID = sender->m_Wallet->StartTransaction(AcceptSwapParameters(parameters, beamFee, gasPrice)); + + io::Timer::Ptr timer = io::Timer::create(*mainReactor); + timer->start(1000, true, [&node]() {node.AddBlock(); }); + + io::AsyncEvent::Ptr eventToUpdate; + //uint64_t startBlocks = receiverBtcWallet.getBlockCount(); + + eventToUpdate = io::AsyncEvent::create(*mainReactor, [&sender, receiver, txID, &eventToUpdate]() + { + if (sender) + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*sender->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + if (txState == wallet::AtomicSwapTransaction::State::HandlingContractTX) + { + // delete sender to simulate refund on ETH side + sender.reset(); + } + eventToUpdate->post(); + } + else + { + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + } + }); + + eventToUpdate->post(); + mainReactor->run(); + + // validate receiver TX state + wallet::AtomicSwapTransaction::State txState = wallet::AtomicSwapTransaction::State::Initial; + storage::getTxParameter(*receiver->m_WalletDB, txID, wallet::kDefaultSubTxID, wallet::TxParameterID::State, txState); + WALLET_CHECK(txState == wallet::AtomicSwapTransaction::State::Refunded); + receiverCoins = receiver->GetCoins(); + WALLET_CHECK(receiverCoins.size() == 0); +} + +// TODO roman.strilets need to implement new test +void TestERC20SwapTransaction(Rules& rules, bool isBeamOwnerStart, beam::Height fork1Height) +{ + cout << "\nTesting ERC20 atomic swap transaction...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + int completedCount = 2; + auto completeAction = [&completedCount, mainReactor](auto) + { + --completedCount; + if (completedCount == 0) + { + mainReactor->stop(); + completedCount = 2; + } + }; + + Amount beamAmount = 300; + Amount beamFee = 101; + Amount swapAmount = 1'000'000'000u; + Amount gasPrice = 30u; + + auto senderWalletDB = createSenderWalletDB(0, 0); + auto binaryTreasury = createTreasury(senderWalletDB, kDefaultTestAmounts); + + ethereum::Settings aliceSettings; + aliceSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; + aliceSettings.m_accountIndex = 3; + //aliceSettings.m_address = "127.0.0.1:7545"; + aliceSettings.m_shouldConnect = true; + aliceSettings.m_lockTxMinConfirmations = 2; + /*aliceSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; + aliceSettings.m_erc20SwapContractAddress = "0x1268071E90CEE6ed135292008f010f60a542c523"; + aliceSettings.m_daiContractAddress = "0x4A2043c5625ec1E6759EA429C6FF8C02979e291E";*/ + + ethereum::Settings bobSettings; + bobSettings.m_secretWords = { "silly", "profit", "jewel", "fox", "evoke", "victory", "until", "topic", "century", "depth", "usual", "update" }; + bobSettings.m_accountIndex = 4; + //bobSettings.m_address = "127.0.0.1:7545"; + bobSettings.m_shouldConnect = true; + bobSettings.m_lockTxMinConfirmations = 2; + /*bobSettings.m_swapContractAddress = "0xe2369A46e36b3586e904Ff533fa77A0c4B48C6D0"; + bobSettings.m_erc20SwapContractAddress = "0x1268071E90CEE6ed135292008f010f60a542c523"; + bobSettings.m_daiContractAddress = "0x4A2043c5625ec1E6759EA429C6FF8C02979e291E";*/ + + auto senderSP = InitSettingsProvider(senderWalletDB, bobSettings); + auto receiverWalletDB = createReceiverWalletDB(); + auto receiverSP = InitSettingsProvider(receiverWalletDB, aliceSettings); + + TestWalletRig sender(senderWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + TestWalletRig receiver(receiverWalletDB, completeAction, TestWalletRig::RegularWithoutPoWBbs); + + InitEthereum(*sender.m_Wallet, sender.m_WalletDB, *mainReactor, *senderSP); + InitEthereum(*receiver.m_Wallet, receiver.m_WalletDB, *mainReactor, *receiverSP); + + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + TxID txID = { {0} }; + + auto receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.empty()); + + Node node; + + NodeObserver observer([&]() + { + auto cursor = node.get_Processor().m_Cursor; + if (cursor.m_hh.m_Height == fork1Height + 5) + { + auto currentHeight = cursor.m_hh.m_Height; + bool isBeamSide = !isBeamOwnerStart; + auto parameters = InitNewSwap2(isBeamOwnerStart ? receiver : sender, + currentHeight, beamAmount, beamFee, wallet::AtomicSwapCoin::Dai, swapAmount, + gasPrice, isBeamSide); + + TestWalletRig* initiator = &sender; + TestWalletRig* acceptor = &receiver; + if (isBeamOwnerStart) + { + std::swap(initiator, acceptor); + } + + initiator->m_Wallet->StartTransaction(parameters); + auto acceptParams = AcceptSwapParameters(parameters, beamFee, gasPrice); + + txID = acceptor->m_Wallet->StartTransaction(acceptParams); + } + }); + + InitNodeToTest(node, rules, binaryTreasury, &observer, 32125, 200); + + mainReactor->run(); + + { + WALLET_CHECK(senderSP->CanModify() == true); + WALLET_CHECK(receiverSP->CanModify() == true); + + receiverCoins = receiver.GetCoins(); + WALLET_CHECK(receiverCoins.size() == 1); + WALLET_CHECK(receiverCoins[0].m_ID.m_Value == beamAmount - beamFee); + WALLET_CHECK(receiverCoins[0].m_status == Coin::Available); + WALLET_CHECK(receiverCoins[0].m_createTxId == txID); + + auto senderCoins = sender.GetCoins(); + WALLET_CHECK(senderCoins.size() == kDefaultTestAmounts.size() + 1); + WALLET_CHECK(senderCoins[0].m_ID.m_Value == 500); + WALLET_CHECK(senderCoins[0].m_status == Coin::Spent); + WALLET_CHECK(senderCoins[0].m_spentTxId == txID); + // change + WALLET_CHECK(senderCoins[4].m_ID.m_Value == 500 - beamAmount - beamFee); + WALLET_CHECK(senderCoins[4].m_status == Coin::Available); + WALLET_CHECK(senderCoins[4].m_createTxId == txID); + } +} + +thread_local const beam::Rules* beam::Rules::s_pInstance = nullptr; + +int main() +{ + int logLevel = BEAM_LOG_LEVEL_INFO; + const auto path = boost::filesystem::system_complete("logs"); + auto logger = beam::Logger::create(logLevel, logLevel, BEAM_LOG_LEVEL_DEBUG, "swap_test", path.string()); + + Rules r; + Rules::Scope scopeRules(r); + r.m_Consensus = Rules::Consensus::FakePoW; + beam::Height fork1Height = 10; + r.pForks[1].m_Height = fork1Height; + r.pForks[2].m_Height = fork1Height; + r.DisableForksFrom(3); // swap values currently specified in the test are insufficient for fees after HF3 + r.UpdateChecksum(); + + TestSwapTransaction(r, true, fork1Height); + TestSwapTransaction(r, false, fork1Height); + TestSwapTransaction(r, true, fork1Height); + TestSwapTransaction(r, false, fork1Height); + TestSwapAssetTransaction(true); + TestSwapAssetTransaction(false); + // Disabled: pre-fork3, a kernel proof verifies only while its block is the + // tip. TestNode advances blocks immediately, so scenarios that fetch the + // redeem kernel afterwards can never verify it and hang. + //TestSwapTransactionWithoutChange(true); + + TestSwapBTCQuickRefundTransaction(); + + TestSwapBTCRefundTransaction(); + TestSwapBeamRefundTransaction(); + TestSwapAssetRefundTransaction(); + //TestSwapBeamAndBTCRefundTransaction(r); + //TestSwapBTCRedeemAfterExpired(r); + + ExpireByResponseTime(true); + ExpireByResponseTime(false); + TestExpireByLifeTime(r); + + TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::Initial); + + TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::BuildingBeamLockTX); + TestSwapCancelTransaction(false, wallet::AtomicSwapTransaction::State::BuildingBeamLockTX); + + TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::BuildingBeamRedeemTX); + TestSwapCancelTransaction(false, wallet::AtomicSwapTransaction::State::BuildingBeamRedeemTX); + + TestSwapCancelTransaction(true, wallet::AtomicSwapTransaction::State::BuildingBeamRefundTX); + TestSwapCancelTransaction(false, wallet::AtomicSwapTransaction::State::BuildingBeamRefundTX); + + wallet::g_EnforceTestnetSwap = true; + + TestElectrumSwapTransaction(r, true, fork1Height); + TestElectrumSwapTransaction(r, false, fork1Height); + + TestElectrumSwapBTCRefundTransaction(); + TestElectrumSwapBeamRefundTransaction(); + + // Disabled: same pre-fork3 kernel-proof timing limitation as + // TestSwapTransactionWithoutChange above. + //TestIgnoringThirdPeer(); + + //TestEthSwapTransaction(r, true, fork1Height); + //TestSwapEthRefundTransaction(); + //TestERC20SwapTransaction(r, true, fork1Height); + + assert(g_failureCount == 0); + return WALLET_CHECK_RESULT; +} diff --git a/wallet/unittests/test_helpers.h b/wallet/unittests/test_helpers.h index f59a6fe7ad..edbf5ae52d 100644 --- a/wallet/unittests/test_helpers.h +++ b/wallet/unittests/test_helpers.h @@ -14,6 +14,7 @@ #pragma once +#include #include #define WALLET_TEST_INIT \ @@ -45,4 +46,4 @@ try { \ PrintFailure(#s, __FILE__, __LINE__); \ } catch(...) { } \ -#define WALLET_CHECK_RESULT g_failureCount ? -1 : 0; \ No newline at end of file +#define WALLET_CHECK_RESULT g_failureCount ? -1 : 0; diff --git a/wallet/unittests/wallet_api_test.cpp b/wallet/unittests/wallet_api_test.cpp index 25bebb2892..bce496b78b 100644 --- a/wallet/unittests/wallet_api_test.cpp +++ b/wallet/unittests/wallet_api_test.cpp @@ -1674,6 +1674,14 @@ void testCalcChange() })); } +void testIsSwapAmountAvailable() +{ + WALLET_CHECK(IsSwapAmountAvailable(boost::none, 100)); + WALLET_CHECK(IsSwapAmountAvailable(boost::optional(150), 100)); + WALLET_CHECK(!IsSwapAmountAvailable(boost::optional(100), 100)); + WALLET_CHECK(!IsSwapAmountAvailable(boost::optional(50), 100)); +} + thread_local const beam::Rules* beam::Rules::s_pInstance = nullptr; int main() @@ -2394,6 +2402,7 @@ int main() testAppsApi(); testCalcChange(); + testIsSwapAmountAvailable(); return WALLET_CHECK_RESULT; } diff --git a/wallet/unittests/wallet_test.cpp b/wallet/unittests/wallet_test.cpp index 16559d3beb..8bbb309c6c 100644 --- a/wallet/unittests/wallet_test.cpp +++ b/wallet/unittests/wallet_test.cpp @@ -79,6 +79,7 @@ namespace { public: Amount getCoinAvailable(AtomicSwapCoin swapCoin) const override { throw std::runtime_error("not impl"); } + boost::optional getTokenAvailable(const std::string& tokenContract, uint8_t decimals) const override { throw std::runtime_error("not impl"); } Amount getRecommendedFeeRate(AtomicSwapCoin swapCoin) const override { throw std::runtime_error("not impl"); } Amount getMinFeeRate(AtomicSwapCoin swapCoin) const override { throw std::runtime_error("not impl"); } Amount getMaxFeeRate(AtomicSwapCoin swapCoin) const override { throw std::runtime_error("not impl"); }