diff --git a/CHANGELOG.md b/CHANGELOG.md index 745143d43..cb0ba4a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## 11.1.0 +- [Add builder for relayed v1 transactions](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/235) + ## 11.0.1 - [Fix construction of AbiRegistry](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/234) diff --git a/package-lock.json b/package-lock.json index 093d583b0..d57992fce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@elrondnetwork/erdjs", - "version": "11.0.1", + "version": "11.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@elrondnetwork/erdjs", - "version": "11.0.1", + "version": "11.1.0", "license": "MIT", "dependencies": { "@elrondnetwork/transaction-decoder": "1.0.0", @@ -1600,9 +1600,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001406", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001406.tgz", - "integrity": "sha512-bWTlaXUy/rq0BBtYShc/jArYfBPjEV95euvZ8JVtO43oQExEN/WquoqpufFjNu4kSpi5cy5kMbNvzztWDfv1Jg==", + "version": "1.0.30001409", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz", + "integrity": "sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ==", "dev": true, "funding": [ { @@ -2136,9 +2136,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.254", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz", - "integrity": "sha512-Sh/7YsHqQYkA6ZHuHMy24e6TE4eX6KZVsZb9E/DvU1nQRIrH4BflO/4k+83tfdYvDl+MObvlqHPRICzEdC9c6Q==", + "version": "1.4.257", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.257.tgz", + "integrity": "sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A==", "dev": true }, "node_modules/elliptic": { @@ -7178,9 +7178,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001406", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001406.tgz", - "integrity": "sha512-bWTlaXUy/rq0BBtYShc/jArYfBPjEV95euvZ8JVtO43oQExEN/WquoqpufFjNu4kSpi5cy5kMbNvzztWDfv1Jg==", + "version": "1.0.30001409", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz", + "integrity": "sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ==", "dev": true }, "chai": { @@ -7636,9 +7636,9 @@ } }, "electron-to-chromium": { - "version": "1.4.254", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz", - "integrity": "sha512-Sh/7YsHqQYkA6ZHuHMy24e6TE4eX6KZVsZb9E/DvU1nQRIrH4BflO/4k+83tfdYvDl+MObvlqHPRICzEdC9c6Q==", + "version": "1.4.257", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.257.tgz", + "integrity": "sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A==", "dev": true }, "elliptic": { diff --git a/package.json b/package.json index 4cabcd335..b4210aa05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@elrondnetwork/erdjs", - "version": "11.0.1", + "version": "11.1.0", "description": "Smart Contracts interaction framework", "main": "out/index.js", "types": "out/index.d.js", diff --git a/src/errors.ts b/src/errors.ts index f8c40d751..65a6eb3f8 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -183,7 +183,7 @@ export class ErrTransactionWatcherTimeout extends Err { } /** - * Signals an issue related to waiting for a specific {@link TransactionStatus}. + * Signals an issue related to waiting for a specific transaction status. */ export class ErrExpectedTransactionStatusNotReached extends Err { public constructor() { @@ -303,6 +303,15 @@ export class ErrNotImplemented extends Err { } } +/** + * Signals invalid arguments when using the relayed v1 builder + */ +export class ErrInvalidRelayedV1BuilderArguments extends Err { + public constructor() { + super("invalid arguments for relayed v1 builder"); + } +} + /** * Signals invalid arguments when using the relayed v2 builder */ diff --git a/src/relayedTransactionV1Builder.spec.ts b/src/relayedTransactionV1Builder.spec.ts new file mode 100644 index 000000000..14b1f401f --- /dev/null +++ b/src/relayedTransactionV1Builder.spec.ts @@ -0,0 +1,77 @@ +import { loadTestWallets, TestWallet } from "./testutils"; +import { assert} from "chai"; +import * as errors from "./errors"; +import { RelayedTransactionV1Builder } from "./relayedTransactionV1Builder"; +import { Transaction } from "./transaction"; +import { Address } from "./address"; +import { TransactionPayload } from "./transactionPayload"; + +describe("test relayed v1 transaction builder", function () { + let alice: TestWallet, bob: TestWallet; + + before(async function () { + ({alice, bob} = await loadTestWallets()); + }); + + it("should throw exception if args were not set", async function () { + const builder = new RelayedTransactionV1Builder(); + assert.throw(() => builder.build(), errors.ErrInvalidRelayedV1BuilderArguments); + + const innerTx = new Transaction({ + nonce: 15, + sender: alice.address, + receiver: Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + gasLimit: 10000000, + chainID: "1", + data: new TransactionPayload("getContractConfig"), + }); + builder.setInnerTransaction(innerTx); + assert.throw(() => builder.build(), errors.ErrInvalidRelayedV1BuilderArguments); + + const networkConfig = { + MinGasLimit: 50_000, + GasPerDataByte: 1_500, + GasPriceModifier: 0.01, + ChainID: "T" + }; + builder.setNetworkConfig(networkConfig); + assert.throw(() => builder.build(), errors.ErrInvalidRelayedV1BuilderArguments); + + builder.setRelayerAddress(alice.getAddress()); + assert.doesNotThrow(() => builder.build()); + }); + + it("should compute relayed v1 transaction", async function () { + const networkConfig = { + MinGasLimit: 50_000, + GasPerDataByte: 1_500, + GasPriceModifier: 0.01, + ChainID: "T" + }; + + const innerTx = new Transaction({ + nonce: 198, + sender: bob.address, + receiver: Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + gasLimit: 60000000, + chainID: networkConfig.ChainID, + data: new TransactionPayload("getContractConfig"), + }); + + await bob.signer.sign(innerTx); + + const builder = new RelayedTransactionV1Builder(); + const relayedTxV1 = builder + .setInnerTransaction(innerTx) + .setRelayerNonce(2627) + .setNetworkConfig(networkConfig) + .setRelayerAddress(alice.address) + .build(); + + await alice.signer.sign(relayedTxV1); + + assert.equal(relayedTxV1.getNonce().valueOf(), 2627); + assert.equal(relayedTxV1.getData().toString(), "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414141415141414141414141414141414141414141414141414141414141432f2f383d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225a3256305132397564484a68593352446232356d6157633d222c227369676e6174757265223a2239682b6e6742584f5536776674315464437368534d4b3454446a5a32794f74686336564c576e3478724d5a706248427738677a6c6659596d362b766b505258303764634a562b4745635462616a7049692b5a5a5942773d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a317d"); + assert.equal(relayedTxV1.getSignature().hex(), "c7d2c3b971f44eca676c10624d3c4319f8898af159f003e1e59f446cb75e5a294c9f0758d800e04d3daff11e67d20c4c1f85fd54aad6deb947ef391e6dd09d07"); + }); +}); diff --git a/src/relayedTransactionV1Builder.ts b/src/relayedTransactionV1Builder.ts new file mode 100644 index 000000000..1ee4f6f9f --- /dev/null +++ b/src/relayedTransactionV1Builder.ts @@ -0,0 +1,113 @@ +import { Transaction } from "./transaction"; +import { IAddress, INonce } from "./interface"; +import { INetworkConfig } from "./interfaceOfNetwork"; +import { ErrInvalidRelayedV1BuilderArguments } from "./errors"; +import { TransactionPayload } from "./transactionPayload"; +import { ContractFunction, StringValue } from "./smartcontracts"; +import { Address } from "./address"; +import BigNumber from "bignumber.js"; + +export class RelayedTransactionV1Builder { + innerTransaction: Transaction | undefined; + relayerAddress: IAddress | undefined; + relayerNonce: INonce | undefined; + netConfig: INetworkConfig | undefined; + + /** + * Sets the inner transaction to be used. It has to be already signed. + * + * @param {Transaction} transaction The inner transaction to be used + */ + setInnerTransaction(transaction: Transaction): RelayedTransactionV1Builder { + this.innerTransaction = transaction; + return this; + } + + /** + * Sets the network config to be used for building the relayed v1 transaction + * + * @param {INetworkConfig} netConfig The network configuration to be used + */ + setNetworkConfig(netConfig: INetworkConfig): RelayedTransactionV1Builder { + this.netConfig = netConfig; + return this; + } + + /** + * Sets the address of the relayer (the one that will actually pay the fee) + * + * @param relayerAddress + */ + setRelayerAddress(relayerAddress: IAddress): RelayedTransactionV1Builder { + this.relayerAddress = relayerAddress; + return this; + } + + /** + * (optional) Sets the nonce of the relayer + * + * @param relayerNonce + */ + setRelayerNonce(relayerNonce: INonce) : RelayedTransactionV1Builder { + this.relayerNonce = relayerNonce; + return this; + } + + /** + * Tries to build the relayed v1 transaction based on the previously set fields + * + * @throws ErrInvalidRelayedV1BuilderArguments + * @return Transaction + */ + build(): Transaction { + if (!this.innerTransaction || !this.netConfig || !this.relayerAddress || !this.innerTransaction.getSignature()) { + throw new ErrInvalidRelayedV1BuilderArguments(); + } + + const serializedTransaction = this.prepareInnerTransaction(); + const payload = TransactionPayload.contractCall() + .setFunction(new ContractFunction("relayedTx")) + .setArgs([ + new StringValue(serializedTransaction), + ]) + .build(); + + const gasLimit = this.netConfig.MinGasLimit + this.netConfig.GasPerDataByte * payload.length() + this.innerTransaction.getGasLimit().valueOf(); + let relayedTransaction = new Transaction({ + nonce: this.relayerNonce, + sender: this.relayerAddress, + receiver: this.innerTransaction.getSender(), + value: 0, + gasLimit: gasLimit, + data: payload, + chainID: this.netConfig.ChainID, + }); + + if (this.relayerNonce) { + relayedTransaction.setNonce(this.relayerNonce); + } + + return relayedTransaction; + } + + private prepareInnerTransaction(): string { + if (!this.innerTransaction) { + return ""; + } + + const txObject = { + "nonce": this.innerTransaction.getNonce().valueOf(), + "sender": new Address(this.innerTransaction.getSender().bech32()).pubkey().toString("base64"), + "receiver": new Address(this.innerTransaction.getReceiver().bech32()).pubkey().toString("base64"), + "value": new BigNumber(this.innerTransaction.getValue().toString(), 10).toNumber(), + "gasPrice": this.innerTransaction.getGasPrice().valueOf(), + "gasLimit": this.innerTransaction.getGasLimit().valueOf(), + "data": this.innerTransaction.getData().valueOf().toString("base64"), + "signature": Buffer.from(this.innerTransaction.getSignature().hex(), 'hex').toString("base64"), + "chainID": Buffer.from(this.innerTransaction.getChainID().valueOf()).toString("base64"), + "version": this.innerTransaction.getVersion().valueOf(), + }; + + return JSON.stringify(txObject); + } +} diff --git a/src/relayedTransactionV2Builder.spec.ts b/src/relayedTransactionV2Builder.spec.ts index 7c4d20433..daf3ec0bd 100644 --- a/src/relayedTransactionV2Builder.spec.ts +++ b/src/relayedTransactionV2Builder.spec.ts @@ -36,10 +36,18 @@ describe("test relayed v2 transaction builder", function () { chainID: networkConfig.ChainID, data: new TransactionPayload("getContractConfig"), }); - builder = builder.setNetworkConfig(networkConfig).setInnerTransactionGasLimit(10).setInnerTransaction(innerTx); + builder = builder + .setNetworkConfig(networkConfig) + .setInnerTransactionGasLimit(10) + .setInnerTransaction(innerTx) + .setRelayerAddress(alice.address); assert.throw(() => builder.build(), errors.ErrGasLimitShouldBe0ForInnerTransaction); - innerTx.setGasLimit({ valueOf: function() { return 10; } }); + innerTx.setGasLimit({ + valueOf: function () { + return 10; + } + }); builder = builder.setNetworkConfig(networkConfig).setInnerTransactionGasLimit(10).setInnerTransaction(innerTx); assert.throw(() => builder.build(), errors.ErrGasLimitShouldBe0ForInnerTransaction); }); @@ -67,10 +75,13 @@ describe("test relayed v2 transaction builder", function () { const relayedTxV2 = builder .setInnerTransaction(innerTx) .setInnerTransactionGasLimit(60_000_000) + .setRelayerNonce(37) .setNetworkConfig(networkConfig) + .setRelayerAddress(alice.getAddress()) .build(); await alice.signer.sign(relayedTxV2); + assert.equal(relayedTxV2.getNonce().valueOf(), 37); assert.equal( relayedTxV2.getData().toString(), "relayedTxV2@000000000000000000010000000000000000000000000000000000000002ffff@0f@676574436f6e7472616374436f6e666967@b6c5262d9837853e2201de357c1cc4c9803988a42d7049d26b7785dd0ac2bd4c6a8804b6fd9cf845fe2c2a622774b1a2dbd0a417c9a0bc3f0563a85bd15e710a"); diff --git a/src/relayedTransactionV2Builder.ts b/src/relayedTransactionV2Builder.ts index 81256c61d..66363fd0f 100644 --- a/src/relayedTransactionV2Builder.ts +++ b/src/relayedTransactionV2Builder.ts @@ -1,15 +1,15 @@ -import {Transaction} from "./transaction"; -import {TransactionPayload} from "./transactionPayload"; -import {AddressValue, BytesValue, ContractFunction, U64Value} from "./smartcontracts"; -import {IAddress, IChainID, IGasLimit, INonce, ITransactionPayload} from "./interface"; -import {ISigner} from "@elrondnetwork/erdjs-walletcore/out/interface"; -import {INetworkConfig} from "./interfaceOfNetwork"; -import {ErrGasLimitShouldBe0ForInnerTransaction, ErrInvalidRelayedV2BuilderArguments} from "./errors"; +import { Transaction } from "./transaction"; +import { TransactionPayload } from "./transactionPayload"; +import { AddressValue, BytesValue, ContractFunction, U64Value } from "./smartcontracts"; +import { IAddress, IGasLimit, INonce } from "./interface"; +import { INetworkConfig } from "./interfaceOfNetwork"; +import { ErrGasLimitShouldBe0ForInnerTransaction, ErrInvalidRelayedV2BuilderArguments } from "./errors"; export class RelayedTransactionV2Builder { - innerTransaction: Transaction | undefined; innerTransactionGasLimit: IGasLimit | undefined; + relayerAddress: IAddress | undefined; + relayerNonce: INonce | undefined; netConfig: INetworkConfig | undefined; /** @@ -18,7 +18,7 @@ export class RelayedTransactionV2Builder { * * @param {Transaction} transaction The inner transaction to be used */ - setInnerTransaction(transaction: Transaction) : RelayedTransactionV2Builder { + setInnerTransaction(transaction: Transaction): RelayedTransactionV2Builder { this.innerTransaction = transaction; return this; } @@ -29,7 +29,7 @@ export class RelayedTransactionV2Builder { * @param {IGasLimit} gasLimit The gas limit to be used. The inner transaction needs to have the gas limit set to 0, * so this field will specify the gas to be used for the SC call of the inner transaction */ - setInnerTransactionGasLimit(gasLimit: IGasLimit) : RelayedTransactionV2Builder { + setInnerTransactionGasLimit(gasLimit: IGasLimit): RelayedTransactionV2Builder { this.innerTransactionGasLimit = gasLimit; return this; } @@ -39,23 +39,44 @@ export class RelayedTransactionV2Builder { * * @param {INetworkConfig} netConfig The network configuration to be used */ - setNetworkConfig(netConfig: INetworkConfig) : RelayedTransactionV2Builder { + setNetworkConfig(netConfig: INetworkConfig): RelayedTransactionV2Builder { this.netConfig = netConfig; return this; } /** - * Tries to build the relayed v2 transaction based on the previously set fields + * Sets the address of the relayer (the one that will actually pay the fee) + * + * @param relayerAddress + */ + setRelayerAddress(relayerAddress: IAddress): RelayedTransactionV2Builder { + this.relayerAddress = relayerAddress; + return this; + } + + /** + * (optional) Sets the nonce of the relayer + * + * @param relayerNonce + */ + setRelayerNonce(relayerNonce: INonce): RelayedTransactionV2Builder { + this.relayerNonce = relayerNonce; + return this; + } + + /** + * Tries to build the relayed v2 transaction based on the previously set fields. + * It returns a transaction that isn't signed * * @throws ErrInvalidRelayedV2BuilderArguments * @throws ErrGasLimitShouldBe0ForInnerTransaction * @return Transaction */ - build() : Transaction { - if(!this.innerTransaction || !this.innerTransactionGasLimit || !this.netConfig || !this.innerTransaction.getSignature()) { + build(): Transaction { + if (!this.innerTransaction || !this.innerTransactionGasLimit || !this.relayerAddress || !this.netConfig || !this.innerTransaction.getSignature()) { throw new ErrInvalidRelayedV2BuilderArguments(); } - if(this.innerTransaction.getGasLimit() != 0){ + if (this.innerTransaction.getGasLimit() != 0) { throw new ErrGasLimitShouldBe0ForInnerTransaction(); } @@ -69,8 +90,8 @@ export class RelayedTransactionV2Builder { ]) .build(); - return new Transaction({ - sender: this.innerTransaction.getSender(), + let relayedTransaction = new Transaction({ + sender: this.relayerAddress, receiver: this.innerTransaction.getSender(), value: 0, gasLimit: @@ -78,5 +99,11 @@ export class RelayedTransactionV2Builder { data: payload, chainID: this.netConfig.ChainID, }); + + if (this.relayerNonce) { + relayedTransaction.setNonce(this.relayerNonce); + } + + return relayedTransaction; } }