diff --git a/package-lock.json b/package-lock.json index 8d9c32d7..75b73116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "13.15.0", + "version": "13.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "13.15.0", + "version": "13.16.0", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", diff --git a/package.json b/package.json index abe0b1d3..8d91ddeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "13.15.0", + "version": "13.16.0", "description": "MultiversX SDK for JavaScript and TypeScript", "author": "MultiversX", "homepage": "https://multiversx.com", diff --git a/src/converters/transactionsConverter.ts b/src/converters/transactionsConverter.ts index 966ec86e..c3798fbe 100644 --- a/src/converters/transactionsConverter.ts +++ b/src/converters/transactionsConverter.ts @@ -1,3 +1,4 @@ +import { Address } from "../address"; import { IPlainTransactionObject, ITransaction } from "../interface"; import { IContractResultItem, ITransactionEvent, ITransactionOnNetwork } from "../interfaceOfNetwork"; import { ResultsParser } from "../smartcontracts"; @@ -25,9 +26,11 @@ export class TransactionsConverter { chainID: transaction.chainID.valueOf(), version: transaction.version, options: transaction.options == 0 ? undefined : transaction.options, + relayer: transaction.relayer.isEmpty() ? undefined : transaction.relayer.toBech32(), guardian: transaction.guardian ? transaction.guardian : undefined, signature: this.toHexOrUndefined(transaction.signature), guardianSignature: this.toHexOrUndefined(transaction.guardianSignature), + relayerSignature: this.toHexOrUndefined(transaction.relayerSignature), }; return plainObject; @@ -46,6 +49,7 @@ export class TransactionsConverter { nonce: BigInt(object.nonce), value: BigInt(object.value || ""), receiver: object.receiver, + relayer: object.relayer ? Address.newFromBech32(object.relayer) : Address.empty(), receiverUsername: this.bufferFromBase64(object.receiverUsername).toString(), sender: object.sender, senderUsername: this.bufferFromBase64(object.senderUsername).toString(), @@ -58,6 +62,7 @@ export class TransactionsConverter { options: Number(object.options), signature: this.bufferFromHex(object.signature), guardianSignature: this.bufferFromHex(object.guardianSignature), + relayerSignature: this.bufferFromHex(object.relayerSignature), }); return transaction; diff --git a/src/converters/transactionsConverters.spec.ts b/src/converters/transactionsConverters.spec.ts index e491ab57..ed143f24 100644 --- a/src/converters/transactionsConverters.spec.ts +++ b/src/converters/transactionsConverters.spec.ts @@ -1,3 +1,5 @@ +import { assert } from "chai"; +import { Address } from "../address"; import { ContractResultItem, ContractResults, @@ -7,8 +9,6 @@ import { TransactionLogsOnNetwork, TransactionOnNetwork, } from "../networkProviders"; -import { assert } from "chai"; -import { Address } from "../address"; import { Transaction } from "../transaction"; import { SmartContractCallOutcome, @@ -58,7 +58,9 @@ describe("test transactions converter", async () => { options: undefined, guardian: undefined, signature: undefined, + relayer: undefined, guardianSignature: undefined, + relayerSignature: undefined, }); }); diff --git a/src/interface.ts b/src/interface.ts index 97c86a54..f91d4b0a 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,4 +1,5 @@ import BigNumber from "bignumber.js"; +import { Address } from "./address"; import { ITransactionOnNetwork } from "./interfaceOfNetwork"; export interface ITransactionFetcher { @@ -16,6 +17,7 @@ export interface IPlainTransactionObject { receiverUsername?: string; senderUsername?: string; guardian?: string; + relayer?: string; gasPrice: number; gasLimit: number; data?: string; @@ -24,6 +26,7 @@ export interface IPlainTransactionObject { options?: number; signature?: string; guardianSignature?: string; + relayerSignature?: string; } export interface ISignature { @@ -102,6 +105,8 @@ export interface ITransaction { version: number; options: number; guardian: string; + relayer: Address; signature: Uint8Array; guardianSignature: Uint8Array; + relayerSignature: Uint8Array; } diff --git a/src/networkProviders/providers.dev.net.spec.ts b/src/networkProviders/providers.dev.net.spec.ts index d08e5b4f..e8a4804a 100644 --- a/src/networkProviders/providers.dev.net.spec.ts +++ b/src/networkProviders/providers.dev.net.spec.ts @@ -452,6 +452,8 @@ describe("test network providers on devnet: Proxy and API", function () { guardian: "", guardianSignature: new Uint8Array(), options: 0, + relayer: Address.empty(), + relayerSignature: new Uint8Array(), }; const apiLegacyTxHash = await apiProvider.sendTransaction(transaction); diff --git a/src/proto/compiled.js b/src/proto/compiled.js index feed796f..5677d707 100644 --- a/src/proto/compiled.js +++ b/src/proto/compiled.js @@ -46,6 +46,8 @@ * @property {number|null} [Options] Transaction Options * @property {Uint8Array|null} [GuardianAddr] Transaction GuardianAddr * @property {Uint8Array|null} [GuardianSignature] Transaction GuardianSignature + * @property {Uint8Array|null} [Relayer] Transaction Relayer + * @property {Uint8Array|null} [RelayerSignature] Transaction RelayerSignature */ /** @@ -183,6 +185,22 @@ */ Transaction.prototype.GuardianSignature = $util.newBuffer([]); + /** + * Transaction Relayer. + * @member {Uint8Array} Relayer + * @memberof proto.Transaction + * @instance + */ + Transaction.prototype.Relayer = $util.newBuffer([]); + + /** + * Transaction RelayerSignature. + * @member {Uint8Array} RelayerSignature + * @memberof proto.Transaction + * @instance + */ + Transaction.prototype.RelayerSignature = $util.newBuffer([]); + /** * Creates a new Transaction instance using the specified properties. * @function create @@ -237,6 +255,10 @@ writer.uint32(/* id 14, wireType 2 =*/114).bytes(message.GuardianAddr); if (message.GuardianSignature != null && Object.hasOwnProperty.call(message, "GuardianSignature")) writer.uint32(/* id 15, wireType 2 =*/122).bytes(message.GuardianSignature); + if (message.Relayer != null && Object.hasOwnProperty.call(message, "Relayer")) + writer.uint32(/* id 16, wireType 2 =*/130).bytes(message.Relayer); + if (message.RelayerSignature != null && Object.hasOwnProperty.call(message, "RelayerSignature")) + writer.uint32(/* id 17, wireType 2 =*/138).bytes(message.RelayerSignature); return writer; }; @@ -331,6 +353,14 @@ message.GuardianSignature = reader.bytes(); break; } + case 16: { + message.Relayer = reader.bytes(); + break; + } + case 17: { + message.RelayerSignature = reader.bytes(); + break; + } default: reader.skipType(tag & 7); break; @@ -411,6 +441,12 @@ if (message.GuardianSignature != null && message.hasOwnProperty("GuardianSignature")) if (!(message.GuardianSignature && typeof message.GuardianSignature.length === "number" || $util.isString(message.GuardianSignature))) return "GuardianSignature: buffer expected"; + if (message.Relayer != null && message.hasOwnProperty("Relayer")) + if (!(message.Relayer && typeof message.Relayer.length === "number" || $util.isString(message.Relayer))) + return "Relayer: buffer expected"; + if (message.RelayerSignature != null && message.hasOwnProperty("RelayerSignature")) + if (!(message.RelayerSignature && typeof message.RelayerSignature.length === "number" || $util.isString(message.RelayerSignature))) + return "RelayerSignature: buffer expected"; return null; }; @@ -507,6 +543,16 @@ $util.base64.decode(object.GuardianSignature, message.GuardianSignature = $util.newBuffer($util.base64.length(object.GuardianSignature)), 0); else if (object.GuardianSignature.length >= 0) message.GuardianSignature = object.GuardianSignature; + if (object.Relayer != null) + if (typeof object.Relayer === "string") + $util.base64.decode(object.Relayer, message.Relayer = $util.newBuffer($util.base64.length(object.Relayer)), 0); + else if (object.Relayer.length >= 0) + message.Relayer = object.Relayer; + if (object.RelayerSignature != null) + if (typeof object.RelayerSignature === "string") + $util.base64.decode(object.RelayerSignature, message.RelayerSignature = $util.newBuffer($util.base64.length(object.RelayerSignature)), 0); + else if (object.RelayerSignature.length >= 0) + message.RelayerSignature = object.RelayerSignature; return message; }; @@ -611,6 +657,20 @@ if (options.bytes !== Array) object.GuardianSignature = $util.newBuffer(object.GuardianSignature); } + if (options.bytes === String) + object.Relayer = ""; + else { + object.Relayer = []; + if (options.bytes !== Array) + object.Relayer = $util.newBuffer(object.Relayer); + } + if (options.bytes === String) + object.RelayerSignature = ""; + else { + object.RelayerSignature = []; + if (options.bytes !== Array) + object.RelayerSignature = $util.newBuffer(object.RelayerSignature); + } } if (message.Nonce != null && message.hasOwnProperty("Nonce")) if (typeof message.Nonce === "number") @@ -651,6 +711,10 @@ object.GuardianAddr = options.bytes === String ? $util.base64.encode(message.GuardianAddr, 0, message.GuardianAddr.length) : options.bytes === Array ? Array.prototype.slice.call(message.GuardianAddr) : message.GuardianAddr; if (message.GuardianSignature != null && message.hasOwnProperty("GuardianSignature")) object.GuardianSignature = options.bytes === String ? $util.base64.encode(message.GuardianSignature, 0, message.GuardianSignature.length) : options.bytes === Array ? Array.prototype.slice.call(message.GuardianSignature) : message.GuardianSignature; + if (message.Relayer != null && message.hasOwnProperty("Relayer")) + object.Relayer = options.bytes === String ? $util.base64.encode(message.Relayer, 0, message.Relayer.length) : options.bytes === Array ? Array.prototype.slice.call(message.Relayer) : message.Relayer; + if (message.RelayerSignature != null && message.hasOwnProperty("RelayerSignature")) + object.RelayerSignature = options.bytes === String ? $util.base64.encode(message.RelayerSignature, 0, message.RelayerSignature.length) : options.bytes === Array ? Array.prototype.slice.call(message.RelayerSignature) : message.RelayerSignature; return object; }; diff --git a/src/proto/serializer.ts b/src/proto/serializer.ts index 2f46ee42..f1693c84 100644 --- a/src/proto/serializer.ts +++ b/src/proto/serializer.ts @@ -60,9 +60,18 @@ export class ProtoSerializer { protoTransaction.GuardianSignature = transaction.guardianSignature; } + if (this.isRelayedTransaction(transaction)) { + protoTransaction.Relayer = transaction.relayer?.getPublicKey(); + protoTransaction.RelayerSignature = transaction.relayerSignature; + } + return protoTransaction; } + private isRelayedTransaction(transaction: ITransaction) { + return !transaction.relayer.isEmpty(); + } + /** * Custom serialization, compatible with mx-chain-go. */ diff --git a/src/proto/transaction.proto b/src/proto/transaction.proto index b3b7dfd1..efc8ad04 100644 --- a/src/proto/transaction.proto +++ b/src/proto/transaction.proto @@ -25,4 +25,6 @@ message Transaction { uint32 Options = 13; bytes GuardianAddr = 14; bytes GuardianSignature = 15; + bytes Relayer = 16; + bytes RelayerSignature = 17; } diff --git a/src/transaction.spec.ts b/src/transaction.spec.ts index ecf98d5c..dfce314e 100644 --- a/src/transaction.spec.ts +++ b/src/transaction.spec.ts @@ -1,4 +1,3 @@ -import { UserPublicKey, UserVerifier } from "./wallet"; import BigNumber from "bignumber.js"; import { assert } from "chai"; import { Address } from "./address"; @@ -10,6 +9,7 @@ import { TokenTransfer } from "./tokens"; import { Transaction } from "./transaction"; import { TransactionComputer } from "./transactionComputer"; import { TransactionPayload } from "./transactionPayload"; +import { UserPublicKey, UserVerifier } from "./wallet"; describe("test transaction", async () => { let wallets: Record; @@ -751,4 +751,44 @@ describe("test transaction", async () => { assert.equal(isSignedByAlice, true); assert.equal(isSignedByBob, false); }); + + it("should serialize transaction with relayer", async () => { + const transaction = new Transaction({ + chainID: networkConfig.ChainID, + sender: wallets.alice.address.toBech32(), + receiver: wallets.alice.address.toBech32(), + relayer: wallets.bob.address, + gasLimit: 50000n, + value: 0n, + version: 2, + nonce: 89n, + }); + + const serializedTransactionBytes = transactionComputer.computeBytesForSigning(transaction); + const serializedTransaction = Buffer.from(serializedTransactionBytes).toString(); + + assert.equal( + serializedTransaction, + `{"nonce":89,"value":"0","receiver":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1000000000,"gasLimit":50000,"chainID":"D","version":2,"relayer":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"}`, + ); + }); + + it("should test relayed v3", async () => { + const transaction = new Transaction({ + chainID: networkConfig.ChainID, + sender: wallets.alice.address.toBech32(), + receiver: wallets.alice.address.toBech32(), + senderUsername: "alice", + receiverUsername: "bob", + gasLimit: 80000n, + value: 0n, + version: 2, + nonce: 89n, + data: Buffer.from("hello"), + }); + + assert.isFalse(transactionComputer.isRelayedV3Transaction(transaction)); + transaction.relayer = wallets.carol.address; + assert.isTrue(transactionComputer.isRelayedV3Transaction(transaction)); + }); }); diff --git a/src/transaction.ts b/src/transaction.ts index a8fad0a9..04aaa92f 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -11,7 +11,6 @@ import { INonce, IPlainTransactionObject, ISignature, - ITransaction, ITransactionOptions, ITransactionPayload, ITransactionValue, @@ -20,8 +19,8 @@ import { import { INetworkConfig } from "./interfaceOfNetwork"; import { TransactionOptions, TransactionVersion } from "./networkParams"; import { interpretSignatureAsBuffer } from "./signature"; -import { TransactionPayload } from "./transactionPayload"; import { TransactionComputer } from "./transactionComputer"; +import { TransactionPayload } from "./transactionPayload"; /** * An abstraction for creating and signing transactions. @@ -92,6 +91,12 @@ export class Transaction { */ public guardian: string; + /** + * The relayer address. + * Note: in the next major version, `sender`, `receiver` and `guardian` will also have the type `Address`, instead of `string`. + */ + public relayer: Address; + /** * The signature. */ @@ -102,6 +107,11 @@ export class Transaction { */ public guardianSignature: Uint8Array; + /** + * The signature of the relayer. + */ + public relayerSignature: Uint8Array; + /** * Creates a new Transaction object. */ @@ -110,6 +120,7 @@ export class Transaction { value?: ITransactionValue | bigint; sender: IAddress | string; receiver: IAddress | string; + relayer?: Address; senderUsername?: string; receiverUsername?: string; gasPrice?: IGasPrice | bigint; @@ -121,6 +132,7 @@ export class Transaction { guardian?: IAddress | string; signature?: Uint8Array; guardianSignature?: Uint8Array; + relayerSignature?: Uint8Array; }) { this.nonce = BigInt(options.nonce?.valueOf() || 0n); // We still rely on "bigNumber" for value, because client code might be passing a BigNumber object as a legacy "ITransactionValue", @@ -137,9 +149,11 @@ export class Transaction { this.version = Number(options.version?.valueOf() || TRANSACTION_VERSION_DEFAULT); this.options = Number(options.options?.valueOf() || TRANSACTION_OPTIONS_DEFAULT); this.guardian = options.guardian ? this.addressAsBech32(options.guardian) : ""; + this.relayer = options.relayer ? options.relayer : Address.empty(); this.signature = options.signature || Buffer.from([]); this.guardianSignature = options.guardianSignature || Buffer.from([]); + this.relayerSignature = options.relayerSignature || Buffer.from([]); } private addressAsBech32(address: IAddress | string): string { diff --git a/src/transactionComputer.ts b/src/transactionComputer.ts index da365e7b..a9690b2f 100644 --- a/src/transactionComputer.ts +++ b/src/transactionComputer.ts @@ -94,6 +94,10 @@ export class TransactionComputer { transaction.guardian = guardian; } + isRelayedV3Transaction(transaction: ITransaction) { + return !transaction.relayer.isEmpty(); + } + applyOptionsForHashSigning(transaction: ITransaction) { if (transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS) { transaction.version = MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS; @@ -122,6 +126,7 @@ export class TransactionComputer { obj.version = transaction.version; obj.options = transaction.options ? transaction.options : undefined; obj.guardian = transaction.guardian ? transaction.guardian : undefined; + obj.relayer = transaction.relayer?.isEmpty() ? undefined : transaction.relayer?.toBech32(); return obj; }