Skip to content

Commit

Permalink
For MultiversX, add support for Relayed V3 transactions.
Browse files Browse the repository at this point in the history
  • Loading branch information
andreibancioiu committed Jan 28, 2025
1 parent b154f9d commit 065820b
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 8 deletions.
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;
}

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

0 comments on commit 065820b

Please sign in to comment.