Skip to content

Commit

Permalink
[THORChainSwap]: Add support for Streaming Swaps (#3385)
Browse files Browse the repository at this point in the history
  • Loading branch information
satoshiotomakan authored Aug 21, 2023
1 parent 0c8e2e5 commit 6dc5acb
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 18 deletions.
8 changes: 6 additions & 2 deletions src/THORChain/Swap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
30 changes: 29 additions & 1 deletion src/THORChain/Swap.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,6 +52,7 @@ class SwapBuilder {
std::optional<std::string> mRouterAddress{std::nullopt};
std::string mFromAmount;
std::string mToAmountLimit{"0"};
std::optional<StreamParams> mStreamParams;
std::optional<std::string> mAffFeeAddress{std::nullopt};
std::optional<std::string> mAffFeeRate{std::nullopt};
std::optional<std::string> mExtraMemo{std::nullopt};
Expand Down Expand Up @@ -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;
}

Expand Down
35 changes: 21 additions & 14 deletions src/THORChain/TWSwap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 16 additions & 1 deletion src/proto/THORChainSwap.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions tests/chains/Cosmos/THORChain/SwapTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
60 changes: 60 additions & 0 deletions tests/chains/Cosmos/THORChain/TWSwapTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(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()));
Expand Down

0 comments on commit 6dc5acb

Please sign in to comment.