From 6dc5acb31f9af687edc99d785c90546f2a3af5bf Mon Sep 17 00:00:00 2001 From: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> Date: Mon, 21 Aug 2023 17:54:31 +0200 Subject: [PATCH] [THORChainSwap]: Add support for Streaming Swaps (#3385) --- src/THORChain/Swap.cpp | 8 ++- src/THORChain/Swap.h | 30 +++++++++- src/THORChain/TWSwap.cpp | 35 ++++++----- src/proto/THORChainSwap.proto | 17 +++++- tests/chains/Cosmos/THORChain/SwapTests.cpp | 12 ++++ tests/chains/Cosmos/THORChain/TWSwapTests.cpp | 60 +++++++++++++++++++ 6 files changed, 144 insertions(+), 18 deletions(-) diff --git a/src/THORChain/Swap.cpp b/src/THORChain/Swap.cpp index 26d9d7b48bf..89e53e4a221 100644 --- a/src/THORChain/Swap.cpp +++ b/src/THORChain/Swap.cpp @@ -144,8 +144,12 @@ std::string SwapBuilder::buildMemo(bool shortened) noexcept { const auto toCoinToken = (!toTokenId.empty() && toTokenId != "0x0000000000000000000000000000000000000000") ? toTokenId : toSymbol; std::stringstream memo; memo << prefix + ":" + chainName(toChain) + "." + toCoinToken + ":" + mToAddress; - if (toAmountLimitNum > 0) { - memo << ":" << std::to_string(toAmountLimitNum); + + memo << ":" << std::to_string(toAmountLimitNum); + if (mStreamParams.has_value()) { + uint64_t intervalNum = std::stoull(mStreamParams->mInterval); + uint64_t quantityNum = std::stoull(mStreamParams->mQuantity); + memo << "/" << std::to_string(intervalNum) << "/" << std::to_string(quantityNum); } if (mAffFeeAddress.has_value() || mAffFeeRate.has_value() || mExtraMemo.has_value()) { diff --git a/src/THORChain/Swap.h b/src/THORChain/Swap.h index 89cd4634575..7f98df6e5d9 100644 --- a/src/THORChain/Swap.h +++ b/src/THORChain/Swap.h @@ -38,6 +38,11 @@ struct SwapBundled { std::string error{""}; }; +struct StreamParams { + std::string mInterval{"1"}; + std::string mQuantity{"0"}; +}; + class SwapBuilder { Proto::Asset mFromAsset; Proto::Asset mToAsset; @@ -47,6 +52,7 @@ class SwapBuilder { std::optional mRouterAddress{std::nullopt}; std::string mFromAmount; std::string mToAmountLimit{"0"}; + std::optional mStreamParams; std::optional mAffFeeAddress{std::nullopt}; std::optional mAffFeeRate{std::nullopt}; std::optional mExtraMemo{std::nullopt}; @@ -128,7 +134,29 @@ class SwapBuilder { } SwapBuilder& toAmountLimit(std::string toAmountLimit) noexcept { - mToAmountLimit = std::move(toAmountLimit); + if (!toAmountLimit.empty()) { + mToAmountLimit = std::move(toAmountLimit); + } + return *this; + } + + SwapBuilder& streamInterval(const std::string& interval) noexcept { + if (!mStreamParams.has_value()) { + mStreamParams = StreamParams(); + } + if (!interval.empty()) { + mStreamParams->mInterval = interval; + } + return *this; + } + + SwapBuilder& streamQuantity(const std::string& quantity) noexcept { + if (!mStreamParams.has_value()) { + mStreamParams = StreamParams(); + } + if (!quantity.empty()) { + mStreamParams->mQuantity = quantity; + } return *this; } diff --git a/src/THORChain/TWSwap.cpp b/src/THORChain/TWSwap.cpp index d511b77c4c2..f1d4b203dc3 100644 --- a/src/THORChain/TWSwap.cpp +++ b/src/THORChain/TWSwap.cpp @@ -24,21 +24,28 @@ TWData* _Nonnull TWTHORChainSwapBuildSwap(TWData* _Nonnull input) { const auto fromChain = inputProto.from_asset().chain(); const auto toChain = inputProto.to_asset().chain(); - auto&& [txInput, errorCode, error] = THORChainSwap::SwapBuilder::builder() - .from(inputProto.from_asset()) - .to(inputProto.to_asset()) - .fromAddress(inputProto.from_address()) - .toAddress(inputProto.to_address()) - .vault(inputProto.vault_address()) - .router(inputProto.router_address()) - .fromAmount(inputProto.from_amount()) - .toAmountLimit(inputProto.to_amount_limit()) - .affFeeAddress(inputProto.affiliate_fee_address()) - .affFeeRate(inputProto.affiliate_fee_rate_bp()) - .extraMemo(inputProto.extra_memo()) - .expirationPolicy(inputProto.expiration_time()) - .build(); + auto builder = THORChainSwap::SwapBuilder::builder(); + builder + .from(inputProto.from_asset()) + .to(inputProto.to_asset()) + .fromAddress(inputProto.from_address()) + .toAddress(inputProto.to_address()) + .vault(inputProto.vault_address()) + .router(inputProto.router_address()) + .fromAmount(inputProto.from_amount()) + .toAmountLimit(inputProto.to_amount_limit()) + .affFeeAddress(inputProto.affiliate_fee_address()) + .affFeeRate(inputProto.affiliate_fee_rate_bp()) + .extraMemo(inputProto.extra_memo()) + .expirationPolicy(inputProto.expiration_time()); + if (inputProto.has_stream_params()) { + const auto& streamParams = inputProto.stream_params(); + builder + .streamInterval(streamParams.interval()) + .streamQuantity(streamParams.quantity()); + } + auto&& [txInput, errorCode, error] = builder.build(); outputProto.set_from_chain(fromChain); outputProto.set_to_chain(toChain); if (errorCode != 0) { diff --git a/src/proto/THORChainSwap.proto b/src/proto/THORChainSwap.proto index 6f32f501142..382c2e25f9b 100644 --- a/src/proto/THORChainSwap.proto +++ b/src/proto/THORChainSwap.proto @@ -56,6 +56,16 @@ message Asset { string token_id = 3; } +message StreamParams { + // Optional Swap Interval ncy in blocks. + // The default is 1 - time-optimised means getting the trade done quickly, regardless of the cost. + string interval = 1; + + // Optional Swap Quantity. Swap interval times every Interval blocks. + // The default is 0 - network will determine the number of swaps. + string quantity = 2; +} + // Input for a swap between source and destination chains; for creating a TX on the source chain. message SwapInput { // Source chain @@ -79,7 +89,8 @@ message SwapInput { // The source amount, integer as string, in the smallest native unit of the chain string from_amount = 7; - // The minimum accepted destination amount. Actual destination amount will depend on current rates, limit amount can be used to prevent using very unfavorable rates. + // Optional minimum accepted destination amount. Actual destination amount will depend on current rates, limit amount can be used to prevent using very unfavorable rates. + // The default is 0 - no price limit. string to_amount_limit = 8; // Optional affiliate fee destination address. A Rune address. @@ -93,6 +104,10 @@ message SwapInput { // Optional expirationTime, will be now() + 15 min if not set uint64 expiration_time = 12; + + // Optional streaming parameters. Use Streaming Swaps and Swap Optimisation strategy if set. + // https://docs.thorchain.org/thorchain-finance/continuous-liquidity-pools#streaming-swaps-and-swap-optimisation + StreamParams stream_params = 13; } // Result of the swap, a SigningInput struct for the specific chain diff --git a/tests/chains/Cosmos/THORChain/SwapTests.cpp b/tests/chains/Cosmos/THORChain/SwapTests.cpp index 6d8568755de..3bcebb89c8c 100644 --- a/tests/chains/Cosmos/THORChain/SwapTests.cpp +++ b/tests/chains/Cosmos/THORChain/SwapTests.cpp @@ -1068,6 +1068,18 @@ TEST(THORChainSwap, Memo) { EXPECT_EQ(builder.to(toAssetBNB).buildMemo(), "=:BNB.BNB:bnb123:1234"); toAssetBNB.set_token_id("TWT-8C2"); EXPECT_EQ(builder.to(toAssetBNB).buildMemo(), "=:BNB.TWT-8C2:bnb123:1234"); + + // Check streaming parameters. + EXPECT_EQ(builder.streamInterval("").buildMemo(), "=:BNB.TWT-8C2:bnb123:1234/1/0"); + EXPECT_EQ(builder.streamQuantity("").buildMemo(), "=:BNB.TWT-8C2:bnb123:1234/1/0"); + EXPECT_EQ(builder.streamQuantity("30").buildMemo(), "=:BNB.TWT-8C2:bnb123:1234/1/30"); + EXPECT_EQ(builder.streamInterval("7").streamQuantity("15").buildMemo(), "=:BNB.TWT-8C2:bnb123:1234/7/15"); + + // Check the default `toAmountLimit` and streaming parameters. + builder = SwapBuilder::builder().to(toAssetETH).toAddress("bnb123"); + builder.to(toAssetBNB); + EXPECT_EQ(builder.buildMemo(), "=:BNB.TWT-8C2:bnb123:0"); + EXPECT_EQ(builder.streamQuantity("").buildMemo(), "=:BNB.TWT-8C2:bnb123:0/1/0"); } TEST(THORChainSwap, WrongFromAddress) { diff --git a/tests/chains/Cosmos/THORChain/TWSwapTests.cpp b/tests/chains/Cosmos/THORChain/TWSwapTests.cpp index 9ea3a9cefa2..9632e015627 100644 --- a/tests/chains/Cosmos/THORChain/TWSwapTests.cpp +++ b/tests/chains/Cosmos/THORChain/TWSwapTests.cpp @@ -340,6 +340,66 @@ TEST(TWTHORChainSwap, SwapRuneDoge) { // https://dogechain.info/tx/905ce02ec3397d6d4f2cbe63ebbff2ccf8b9f16d7ea136319be5ed543cdb66f3 } +TEST(TWTHORChainSwap, SwapRuneBnbStreamParams) { + // prepare swap input + Proto::SwapInput input; + Proto::Asset fromAsset; + fromAsset.set_chain(Proto::THOR); + fromAsset.set_symbol("RUNE"); + *input.mutable_from_asset() = fromAsset; + input.set_from_address("thor157vzvw2chydgf8g4qu2cqhlsyhq0mydutmd0p7"); + Proto::Asset toAsset; + toAsset.set_chain(Proto::BNB); + toAsset.set_symbol("BNB"); + *input.mutable_to_asset() = toAsset; + input.set_to_address("bnb1swlv73yc6rc7z4n244gcpjknqh22m7kpjpr0mw"); + input.set_from_amount("170000000"); + // Don't set `toAmountLimit`, should be 0 by default. + auto* streamParams = input.mutable_stream_params(); + streamParams->set_interval("1"); + streamParams->set_quantity("0"); + input.set_affiliate_fee_address("tr"); + input.set_affiliate_fee_rate_bp("0"); + + // serialize input + const auto inputData_ = input.SerializeAsString(); + EXPECT_EQ(hex(inputData_), "0a06120452554e45122b74686f72313537767a7677326368796467663867347175326371686c73796871306d796475746d643070371a0708031203424e42222a626e623173776c7637337963367263377a346e3234346763706a6b6e716832326d376b706a7072306d773a093137303030303030304a0274725201306a060a0131120130"); + const auto inputTWData_ = WRAPD(TWDataCreateWithBytes((const uint8_t *)inputData_.data(), inputData_.size())); + + // invoke swap + const auto outputTWData_ = WRAPD(TWTHORChainSwapBuildSwap(inputTWData_.get())); + const auto outputData = data(TWDataBytes(outputTWData_.get()), TWDataSize(outputTWData_.get())); + EXPECT_EQ(outputData.size(), 156ul); + // parse result in proto + Proto::SwapOutput outputProto; + EXPECT_TRUE(outputProto.ParseFromArray(outputData.data(), static_cast(outputData.size()))); + EXPECT_EQ(outputProto.from_chain(), Proto::THOR); + EXPECT_EQ(outputProto.to_chain(), Proto::BNB); + EXPECT_EQ(outputProto.error().code(), 0); + EXPECT_EQ(outputProto.error().message(), ""); + EXPECT_TRUE(outputProto.has_cosmos()); + Cosmos::Proto::SigningInput txInput = outputProto.cosmos(); + + ASSERT_EQ(txInput.messages(0).thorchain_deposit_message().memo(), "=:BNB.BNB:bnb1swlv73yc6rc7z4n244gcpjknqh22m7kpjpr0mw:0/1/0:tr:0"); + auto& fee = *txInput.mutable_fee(); + fee.set_gas(50000000); + + txInput.set_account_number(76456); + txInput.set_sequence(0); + + auto privKey = parse_hex("15f9be0e6c80949f3dbe24fd9614027869af1e41953a86fdced947b0b1f3efa7"); + txInput.set_private_key(privKey.data(), privKey.size()); + + // sign and encode resulting input + Cosmos::Proto::SigningOutput output; + ANY_SIGN(txInput, TWCoinTypeCosmos); + EXPECT_EQ(output.error_message(), ""); + ASSERT_EQ(output.serialized(), "{\"mode\":\"BROADCAST_MODE_BLOCK\",\"tx_bytes\":\"CpABCo0BChEvdHlwZXMuTXNnRGVwb3NpdBJ4Ch8KEgoEVEhPUhIEUlVORRoEUlVORRIJMTcwMDAwMDAwEj89OkJOQi5CTkI6Ym5iMXN3bHY3M3ljNnJjN3o0bjI0NGdjcGprbnFoMjJtN2twanByMG13OjAvMS8wOnRyOjAaFKeYJjlYuRqEnRUHFYBf8CXA/ZG8ElcKTgpGCh8vY29zbW9zLmNyeXB0by5zZWNwMjU2azEuUHViS2V5EiMKIQNWwhqmW30kANTyAfdGJPa9BfZlI3xkAjqLWmhynukWThIECgIIARIFEIDh6xcaQNzvOBmgAgRriO5lsEgU4o58Gxu4mA71XZNyf5XXWBo5L9HkaJiDXE/YOlWPFj7iy86vDXVR1798pmc3n5EbkQ0=\"}"); + + // https://viewblock.io/thorchain/tx/317443DD48DDEE8811D0DCCC2FCA397F8E93DA0AC9C1D5173CB42E69CD0E01B0 + // https://explorer.bnbchain.org/tx/6DE7B60C71F9FC3EEE914AAD8FE80D1A53A2EC59BE759A1C111C1B6C194740D2 +} + TEST(TWTHORChainSwap, NegativeInvalidInput) { const auto inputData = parse_hex("00112233"); const auto inputTWData = WRAPD(TWDataCreateWithBytes((const uint8_t *)inputData.data(), inputData.size()));