Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MultiversX]: Add support for relayed (V3) transactions #4243

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,38 @@ class TestMultiversXSigner {
assertEquals("""{"nonce":42,"value":"1000000000000000000","receiver":"$bobBech32","sender":"$aliceBech32","gasPrice":1000000000,"gasLimit":100000,"chainID":"1","version":2,"signature":"$expectedSignature","options":2,"guardian":"$carolBech32"}""", output.encoded)
}

@Test
fun signGenericActionWithRelayer() {
val privateKey = ByteString.copyFrom(PrivateKey(aliceSeedHex.toHexByteArray()).data())

val accounts = MultiversX.Accounts.newBuilder()
.setSenderNonce(42)
.setSender(aliceBech32)
.setReceiver(bobBech32)
.setRelayer(carolBech32)
.build()

val genericAction = MultiversX.GenericAction.newBuilder()
.setAccounts(accounts)
.setValue("1000000000000000000")
.setVersion(2)
.build()

val signingInput = MultiversX.SigningInput.newBuilder()
.setGenericAction(genericAction)
.setGasPrice(1000000000)
.setGasLimit(100000)
.setChainId("1")
.setPrivateKey(privateKey)
.build()

val output = AnySigner.sign(signingInput, CoinType.MULTIVERSX, MultiversX.SigningOutput.parser())
val expectedSignature = "f0137ce0303a33814691975598dab3b82bb91b017aa251640a48827edc48048aa0f916dd3e7915dd3be27db3304fc238a719123b6ae2285731ab24b794665003"

assertEquals(expectedSignature, output.signature)
assertEquals("""{"nonce":42,"value":"1000000000000000000","receiver":"$bobBech32","sender":"$aliceBech32","gasPrice":1000000000,"gasLimit":100000,"chainID":"1","version":2,"signature":"$expectedSignature","relayer":"$carolBech32"}""", output.encoded)
}

@Test
fun signGenericActionUndelegate() {
// Successfully broadcasted https://explorer.multiversx.com/transactions/3301ae5a6a77f0ab9ceb5125258f12539a113b0c6787de76a5c5867f2c515d65
Expand Down
7 changes: 6 additions & 1 deletion src/MultiversX/Serialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ std::map<std::string, int> fields_order{
{"version", 11},
{"signature", 12},
{"options", 13},
{"guardian", 14}};
{"guardian", 14},
{"relayer", 15}};

struct FieldsSorter {
bool operator()(const std::string& lhs, const std::string& rhs) const {
Expand Down Expand Up @@ -69,6 +70,10 @@ sorted_json preparePayload(const MultiversX::Transaction& transaction) {
payload["guardian"] = json(transaction.guardian);
}

if (!transaction.relayer.empty()) {
payload["relayer"] = json(transaction.relayer);
}

return payload;
}

Expand Down
6 changes: 5 additions & 1 deletion src/MultiversX/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
namespace TW::MultiversX {

Transaction::Transaction()
: nonce(0), sender(""), senderUsername(""), receiver(""), receiverUsername(""), guardian(""), value("0"), data(""), gasPrice(0), gasLimit(0), chainID(""), version(0), options(TransactionOptions::Default) {
: nonce(0), sender(""), senderUsername(""), receiver(""), receiverUsername(""), guardian(""), relayer(""), value("0"), data(""), gasPrice(0), gasLimit(0), chainID(""), version(0), options(TransactionOptions::Default) {
}

bool Transaction::hasGuardian() const {
return !guardian.empty();
}

bool Transaction::hasRelayer() const {
return !relayer.empty();
}

} // namespace TW::MultiversX
2 changes: 2 additions & 0 deletions src/MultiversX/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Transaction {
std::string receiver;
std::string receiverUsername;
std::string guardian;
std::string relayer;
std::string value;
std::string data;
uint64_t gasPrice;
Expand All @@ -37,6 +38,7 @@ class Transaction {
Transaction();

bool hasGuardian() const;
bool hasRelayer() const;
};

} // namespace TW::MultiversX
16 changes: 12 additions & 4 deletions src/MultiversX/TransactionFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Transaction TransactionFactory::fromGenericAction(const Proto::SigningInput& inp
transaction.receiver = action.accounts().receiver();
transaction.receiverUsername = action.accounts().receiver_username();
transaction.guardian = action.accounts().guardian();
transaction.relayer = action.accounts().relayer();
transaction.value = action.value();
transaction.data = action.data();
transaction.gasLimit = input.gas_limit();
Expand All @@ -62,6 +63,7 @@ Transaction TransactionFactory::fromEGLDTransfer(const Proto::SigningInput& inpu
transaction.receiver = transfer.accounts().receiver();
transaction.receiverUsername = transfer.accounts().receiver_username();
transaction.guardian = transfer.accounts().guardian();
transaction.relayer = transfer.accounts().relayer();
transaction.value = transfer.amount();
transaction.data = transfer.data();
transaction.gasPrice = coalesceGasPrice(input.gas_price());
Expand All @@ -70,7 +72,7 @@ Transaction TransactionFactory::fromEGLDTransfer(const Proto::SigningInput& inpu
transaction.options = decideOptions(transaction);

// Estimate & set gasLimit:
uint64_t estimatedGasLimit = computeGasLimit(0, 0, transaction.hasGuardian());
uint64_t estimatedGasLimit = computeGasLimit(0, 0, transaction.hasGuardian(), transaction.hasRelayer());
transaction.gasLimit = coalesceGasLimit(input.gas_limit(), estimatedGasLimit);

return transaction;
Expand All @@ -90,6 +92,7 @@ Transaction TransactionFactory::fromESDTTransfer(const Proto::SigningInput& inpu
transaction.receiver = transfer.accounts().receiver();
transaction.receiverUsername = transfer.accounts().receiver_username();
transaction.guardian = transfer.accounts().guardian();
transaction.relayer = transfer.accounts().relayer();
transaction.value = "0";
transaction.data = data;
transaction.gasPrice = coalesceGasPrice(input.gas_price());
Expand All @@ -99,7 +102,7 @@ Transaction TransactionFactory::fromESDTTransfer(const Proto::SigningInput& inpu

// Estimate & set gasLimit:
uint64_t executionGasLimit = this->config.getGasCostESDTTransfer() + this->config.getAdditionalGasForESDTTransfer();
uint64_t estimatedGasLimit = computeGasLimit(data.size(), executionGasLimit, transaction.hasGuardian());
uint64_t estimatedGasLimit = computeGasLimit(data.size(), executionGasLimit, transaction.hasGuardian(), transaction.hasRelayer());
transaction.gasLimit = coalesceGasLimit(input.gas_limit(), estimatedGasLimit);

return transaction;
Expand All @@ -120,6 +123,7 @@ Transaction TransactionFactory::fromESDTNFTTransfer(const Proto::SigningInput& i
transaction.sender = transfer.accounts().sender();
transaction.receiver = transfer.accounts().sender();
transaction.guardian = transfer.accounts().guardian();
transaction.relayer = transfer.accounts().relayer();
transaction.value = "0";
transaction.data = data;
transaction.gasPrice = coalesceGasPrice(input.gas_price());
Expand All @@ -129,20 +133,24 @@ Transaction TransactionFactory::fromESDTNFTTransfer(const Proto::SigningInput& i

// Estimate & set gasLimit:
uint64_t executionGasLimit = this->config.getGasCostESDTNFTTransfer() + this->config.getAdditionalGasForESDTNFTTransfer();
uint64_t estimatedGasLimit = computeGasLimit(data.size(), executionGasLimit, transaction.hasGuardian());
uint64_t estimatedGasLimit = computeGasLimit(data.size(), executionGasLimit, transaction.hasGuardian(), transaction.hasRelayer());
transaction.gasLimit = coalesceGasLimit(input.gas_limit(), estimatedGasLimit);

return transaction;
}

uint64_t TransactionFactory::computeGasLimit(size_t dataLength, uint64_t executionGasLimit, bool hasGuardian) {
uint64_t TransactionFactory::computeGasLimit(size_t dataLength, uint64_t executionGasLimit, bool hasGuardian, bool hasRelayer) {
uint64_t dataMovementGasLimit = this->config.getMinGasLimit() + this->config.getGasPerDataByte() * dataLength;
uint64_t gasLimit = dataMovementGasLimit + executionGasLimit;

if (hasGuardian) {
gasLimit += this->config.getExtraGasLimitForGuardedTransaction();
}

if (hasRelayer) {
gasLimit += this->config.getExtraGasLimitForRelayedTransaction();
}

return gasLimit;
}

Expand Down
2 changes: 1 addition & 1 deletion src/MultiversX/TransactionFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class TransactionFactory {
Transaction fromESDTNFTTransfer(const Proto::SigningInput& input);

private:
uint64_t computeGasLimit(size_t dataLength, uint64_t executionGasLimit, bool hasGuardian);
uint64_t computeGasLimit(size_t dataLength, uint64_t executionGasLimit, bool hasGuardian, bool hasRelayer);
uint64_t coalesceGasLimit(uint64_t providedGasLimit, uint64_t estimatedGasLimit);
uint64_t coalesceGasPrice(uint64_t gasPrice);
std::string coalesceChainId(std::string chainID);
Expand Down
9 changes: 9 additions & 0 deletions src/MultiversX/TransactionFactoryConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ void TransactionFactoryConfig::setExtraGasLimitForGuardedTransaction(uint32_t va
this->extraGasLimitForGuardedTransaction = value;
}

uint32_t TransactionFactoryConfig::getExtraGasLimitForRelayedTransaction() const {
return this->minGasLimit;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't extraGasLimitForRelayedTransaction be returned here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, thank you 🙏

Thus, fixed getExtraGasLimitForRelayedTransaction and getExtraGasLimitForGuardedTransaction.


void TransactionFactoryConfig::setExtraGasLimitForRelayedTransaction(uint32_t value) {
this->extraGasLimitForRelayedTransaction = value;
}

uint64_t TransactionFactoryConfig::getMinGasPrice() const {
return this->minGasPrice;
}
Expand Down Expand Up @@ -97,6 +105,7 @@ TransactionFactoryConfig TransactionFactoryConfig::GetByTimestamp(uint64_t times
config.setGasPerDataByte(1500);
config.setMinGasLimit(50000);
config.setExtraGasLimitForGuardedTransaction(50000);
config.setExtraGasLimitForRelayedTransaction(50000);
config.setMinGasPrice(1000000000);
config.setGasCostESDTTransfer(200000);
config.setGasCostESDTNFTTransfer(200000);
Expand Down
4 changes: 4 additions & 0 deletions src/MultiversX/TransactionFactoryConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class TransactionFactoryConfig {
uint32_t gasPerDataByte;
uint32_t minGasLimit;
uint32_t extraGasLimitForGuardedTransaction;
uint32_t extraGasLimitForRelayedTransaction;
uint64_t minGasPrice;

/// GasSchedule entries of interest (only one at this moment), according to: https://github.com/multiversx/mx-chain-mainnet-config/blob/master/gasSchedules.
Expand Down Expand Up @@ -47,6 +48,9 @@ class TransactionFactoryConfig {
uint32_t getExtraGasLimitForGuardedTransaction() const;
void setExtraGasLimitForGuardedTransaction(uint32_t value);

uint32_t getExtraGasLimitForRelayedTransaction() const;
void setExtraGasLimitForRelayedTransaction(uint32_t value);

uint64_t getMinGasPrice() const;
void setMinGasPrice(uint64_t value);

Expand Down
3 changes: 3 additions & 0 deletions src/proto/MultiversX.proto
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ message Accounts {

// Guardian address
string guardian = 6;

// Relayer address
string relayer = 7;
}

// Input data necessary to create a signed transaction.
Expand Down
29 changes: 29 additions & 0 deletions swift/Tests/Blockchains/MultiversXTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,35 @@ class MultiversXTests: XCTestCase {
XCTAssertEqual(output.signature, expectedSignature)
XCTAssertEqual(output.encoded, expectedEncoded)
}

func testSignGenericActionWithRelayer() {
let privateKey = PrivateKey(data: Data(hexString: aliceSeedHex)!)!

let input = MultiversXSigningInput.with {
$0.genericAction = MultiversXGenericAction.with {
$0.accounts = MultiversXAccounts.with {
$0.senderNonce = 42
$0.sender = aliceBech32
$0.receiver = bobBech32
$0.relayer = carolBech32
}
$0.value = "1000000000000000000"
$0.data = ""
$0.version = 2
}
$0.gasPrice = 1000000000
$0.gasLimit = 100000
$0.chainID = "1"
$0.privateKey = privateKey.data
}

let output: MultiversXSigningOutput = AnySigner.sign(input: input, coin: .multiversX)
let expectedSignature = "f0137ce0303a33814691975598dab3b82bb91b017aa251640a48827edc48048aa0f916dd3e7915dd3be27db3304fc238a719123b6ae2285731ab24b794665003"
let expectedEncoded = #"{"nonce":42,"value":"1000000000000000000","receiver":"\#(bobBech32)","sender":"\#(aliceBech32)","gasPrice":1000000000,"gasLimit":100000,"chainID":"1","version":2,"signature":"\#(expectedSignature)","relayer":"\#(carolBech32)"}"#

XCTAssertEqual(output.signature, expectedSignature)
XCTAssertEqual(output.encoded, expectedEncoded)
}

func testSignGenericActionUndelegate() {
// Successfully broadcasted https://explorer.multiversx.com/transactions/3301ae5a6a77f0ab9ceb5125258f12539a113b0c6787de76a5c5867f2c515d65
Expand Down
25 changes: 25 additions & 0 deletions tests/chains/MultiversX/SerializationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,29 @@ TEST(MultiversXSerialization, SerializeTransactionWithGuardianAddress) {
ASSERT_EQ(expected, actual);
}

TEST(MultiversXSerialization, SerializeTransactionWithRelayerAddress) {
Transaction transaction;
transaction.nonce = 42;
transaction.value = "1000000000000000000";
transaction.sender = ALICE_BECH32;
transaction.receiver = BOB_BECH32;
transaction.relayer = CAROL_BECH32;
transaction.gasPrice = 1000000000;
transaction.gasLimit = 100000;
transaction.chainID = "1";
transaction.version = 2;

string expected =
"{"
R"("nonce":42,"value":"1000000000000000000",)"
R"("receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",)"
R"("sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",)"
R"("gasPrice":1000000000,"gasLimit":100000,"chainID":"1","version":2,)"
R"("relayer":"erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8")"
"}";

string actual = serializeTransaction(transaction);
ASSERT_EQ(expected, actual);
}

} // namespace TW::MultiversX::tests
73 changes: 72 additions & 1 deletion tests/chains/MultiversX/SignerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>

#include "boost/format.hpp"
#include "HexCoding.h"
#include "MultiversX/Address.h"
#include "MultiversX/Codec.h"
Expand All @@ -15,6 +14,7 @@
#include "PublicKey.h"
#include "TestAccounts.h"
#include "TestUtilities.h"
#include "boost/format.hpp"

using namespace TW;

Expand Down Expand Up @@ -608,6 +608,77 @@ TEST(MultiversXSigner, SignEGLDTransferWithGuardian) {
assertJSONEqual(expected, nlohmann::json::parse(encoded));
}

TEST(MultiversXSigner, SignGenericActionWithRelayer) {
auto input = Proto::SigningInput();
auto privateKey = PrivateKey(parse_hex(ALICE_SEED_HEX));
input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size());

input.mutable_generic_action()->mutable_accounts()->set_sender_nonce(42);
input.mutable_generic_action()->mutable_accounts()->set_sender(ALICE_BECH32);
input.mutable_generic_action()->mutable_accounts()->set_receiver(BOB_BECH32);
input.mutable_generic_action()->mutable_accounts()->set_relayer(CAROL_BECH32);
input.mutable_generic_action()->set_value("1000000000000000000");
input.mutable_generic_action()->set_data("");
input.mutable_generic_action()->set_version(2);
input.set_gas_price(1000000000);
input.set_gas_limit(100000);
input.set_chain_id("1");

auto output = Signer::sign(input);
auto signature = output.signature();
auto encoded = output.encoded();
auto expectedSignature = "f0137ce0303a33814691975598dab3b82bb91b017aa251640a48827edc48048aa0f916dd3e7915dd3be27db3304fc238a719123b6ae2285731ab24b794665003";
nlohmann::json expected = R"(
{
"chainID":"1",
"gasLimit":100000,
"gasPrice":1000000000,
"relayer":"erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8",
"nonce":42,
"receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
"signature":"f0137ce0303a33814691975598dab3b82bb91b017aa251640a48827edc48048aa0f916dd3e7915dd3be27db3304fc238a719123b6ae2285731ab24b794665003",
"value":"1000000000000000000",
"version":2
})"_json;

ASSERT_EQ(expectedSignature, signature);
assertJSONEqual(expected, nlohmann::json::parse(encoded));
}

TEST(MultiversXSigner, SignEGLDTransferWithRelayer) {
auto input = Proto::SigningInput();
auto privateKey = PrivateKey(parse_hex(ALICE_SEED_HEX));
input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size());

input.mutable_egld_transfer()->mutable_accounts()->set_sender_nonce(7);
input.mutable_egld_transfer()->mutable_accounts()->set_sender(ALICE_BECH32);
input.mutable_egld_transfer()->mutable_accounts()->set_receiver(BOB_BECH32);
input.mutable_egld_transfer()->mutable_accounts()->set_relayer(CAROL_BECH32);
input.mutable_egld_transfer()->set_amount("1000000000000000000");

auto output = Signer::sign(input);
auto signature = output.signature();
auto encoded = output.encoded();
auto expectedSignature = "c86491a51d553889df9fb7ff75880843e2b21aec97ae3e4004b70801a5494a8958af8daf56906f9720b0af6a25ad2ab82b3af05940fb6dfe0dea529f1bf8d90f";
nlohmann::json expected = R"(
{
"chainID":"1",
"gasLimit":100000,
"gasPrice":1000000000,
"relayer":"erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8",
"nonce":7,
"receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
"signature":"c86491a51d553889df9fb7ff75880843e2b21aec97ae3e4004b70801a5494a8958af8daf56906f9720b0af6a25ad2ab82b3af05940fb6dfe0dea529f1bf8d90f",
"value":"1000000000000000000",
"version":2
})"_json;

ASSERT_EQ(expectedSignature, signature);
assertJSONEqual(expected, nlohmann::json::parse(encoded));
}

TEST(ElrondSigner, buildUnsignedTxBytes) {
auto input = Proto::SigningInput();
input.mutable_generic_action()->mutable_accounts()->set_sender_nonce(7);
Expand Down
Loading