From f1132660136a429585483991755c3f167b115476 Mon Sep 17 00:00:00 2001 From: danielailie Date: Thu, 3 Oct 2024 17:07:15 +0300 Subject: [PATCH] Add inner transaction for relay v3 --- src/networkProviders/interface.ts | 25 +++-- .../providers.dev.net.spec.ts | 103 ++++++++++++++++++ src/networkProviders/transactions.ts | 55 ++++++++-- 3 files changed, 169 insertions(+), 14 deletions(-) diff --git a/src/networkProviders/interface.ts b/src/networkProviders/interface.ts index 144f0583..15d02324 100644 --- a/src/networkProviders/interface.ts +++ b/src/networkProviders/interface.ts @@ -46,7 +46,10 @@ export interface INetworkProvider { /** * Fetches data about the non-fungible tokens held by account. */ - getNonFungibleTokensOfAccount(address: IAddress, pagination?: IPagination): Promise; + getNonFungibleTokensOfAccount( + address: IAddress, + pagination?: IPagination, + ): Promise; /** * Fetches data about a specific fungible token held by an account. @@ -56,7 +59,11 @@ export interface INetworkProvider { /** * Fetches data about a specific non-fungible token (instance) held by an account. */ - getNonFungibleTokenOfAccount(address: IAddress, collection: string, nonce: number): Promise; + getNonFungibleTokenOfAccount( + address: IAddress, + collection: string, + nonce: number, + ): Promise; /** * Fetches the state of a transaction. @@ -80,7 +87,7 @@ export interface INetworkProvider { /** * Simulates the processing of an already-signed transaction. - * + * */ simulateTransaction(tx: ITransaction): Promise; @@ -118,8 +125,8 @@ export interface INetworkProvider { export interface IContractQuery { address: IAddress; caller?: IAddress; - func: { toString(): string; }; - value?: { toString(): string; }; + func: { toString(): string }; + value?: { toString(): string }; getEncodedArguments(): string[]; } @@ -132,7 +139,9 @@ export interface ITransaction { toSendable(): any; } -export interface IAddress { bech32(): string; } +export interface IAddress { + bech32(): string; +} export interface ITransactionNext { sender: string; @@ -150,4 +159,6 @@ export interface ITransactionNext { guardian: string; signature: Uint8Array; guardianSignature: Uint8Array; - } + relayer?: string; + innerTransactions?: ITransactionNext[]; +} diff --git a/src/networkProviders/providers.dev.net.spec.ts b/src/networkProviders/providers.dev.net.spec.ts index 354cc67a..954b003b 100644 --- a/src/networkProviders/providers.dev.net.spec.ts +++ b/src/networkProviders/providers.dev.net.spec.ts @@ -271,6 +271,49 @@ describe("test network providers on devnet: Proxy and API", function () { } }); + it("should have same response for getTransaction() (relayed V3)", async function () { + this.timeout(20000); + + // Transaction was sent using mxpy, as follows: + // mxpy tx new --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem --receiver=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx --value=1000000000000000000 --gas-limit=50000 --recall-nonce --relayer=erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede --inner-transactions-outfile=inner.json --proxy=https://devnet-gateway.multiversx.com + // mxpy tx new --pem=~/multiversx-sdk/testwallets/latest/users/grace.pem --receiver=erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede --gas-limit=100000 --recall-nonce --chain=D --inner-transactions=inner.json --proxy=https://devnet-gateway.multiversx.com --send + + const txHash = "8cdfb790be8cd4b331da486ba014ed56d0dbac70c1cfbadf11db2edd48d0e437"; + const apiResponse = await apiProvider.getTransaction(txHash); + const proxyResponse = await proxyProvider.getTransaction(txHash, true); + + assert.deepEqual(proxyResponse.innerTransactions, [ + { + nonce: BigInt(9340), + value: BigInt("1000000000000000000"), + receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + data: Buffer.from([]), + gasPrice: BigInt(1000000000), + gasLimit: BigInt(50000), + chainID: "D", + version: 2, + signature: Buffer.from( + "0993c2f3a47c01cf8330e54571ea9340aae481d0d5212af31b62eb7194e199231f105134aae28a75bb48b53a3dff09d6c6208843c8e0376617cf62d3bfb60204", + "hex", + ), + senderUsername: "", + receiverUsername: "", + guardian: "", + guardianSignature: Buffer.from([]), + options: 0, + relayer: "erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede", + }, + ]); + + ignoreKnownTransactionDifferencesBetweenProviders(apiResponse, proxyResponse); + assert.deepEqual(apiResponse, proxyResponse); + + // Also assert completion + assert.isTrue(apiResponse.isCompleted); + assert.isTrue(proxyResponse.isCompleted); + }); + // TODO: Strive to have as little differences as possible between Proxy and API. function ignoreKnownTransactionDifferencesBetweenProviders( apiResponse: TransactionOnNetwork, @@ -284,8 +327,68 @@ describe("test network providers on devnet: Proxy and API", function () { proxyResponse.blockNonce = 0; proxyResponse.hyperblockNonce = 0; proxyResponse.hyperblockHash = ""; + + // API does not provide "innerTransactions" (Spica), for the moment. + proxyResponse.innerTransactions = []; } + it("should send `TransactionNext` (as relayed V3)", async function () { + this.timeout(50000); + + // Transaction was created using mxpy, as follows: + // mxpy tx new --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem --receiver=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx --value=1000000000000000000 --gas-limit=50000 --nonce=7 --chain=D --relayer=erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede --inner-transactions-outfile=inner.json + // mxpy tx new --pem=~/multiversx-sdk/testwallets/latest/users/grace.pem --receiver=erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede --gas-limit=100000 --nonce=42 --chain=D --inner-transactions=inner.json + + const transactionNext: ITransactionNext = { + nonce: BigInt(42), + value: BigInt(0), + receiver: "erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede", + sender: "erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede", + data: new Uint8Array(), + gasPrice: BigInt(1000000000), + gasLimit: BigInt(100000), + chainID: "D", + version: 2, + signature: Buffer.from( + "c623854967c954d13681035d5b24be68a5a58d25e7efdc75c7d59b5c389e1ad6c9d21a6f41149ec6e8bd051d74f3636a60ce047062f05c748600c36348238e0b", + "hex", + ), + senderUsername: "", + receiverUsername: "", + guardian: "", + guardianSignature: new Uint8Array(), + options: 0, + innerTransactions: [ + { + nonce: BigInt(7), + value: BigInt("1000000000000000000"), + receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + data: new Uint8Array(), + gasPrice: BigInt(1000000000), + gasLimit: BigInt(50000), + chainID: "D", + version: 2, + signature: Buffer.from( + "40808231154b9924c0d5f885d320f4ab666308f7443ea128ac26029b1de07abfcee6412e1249a9c0fcf79638d9691be3c9fe75dd7c85462082f9b86c4008b30e", + "hex", + ), + senderUsername: "", + receiverUsername: "", + guardian: "", + guardianSignature: new Uint8Array(), + options: 0, + relayer: "erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede", + }, + ], + }; + + const apiTxNextHash = await apiProvider.sendTransaction(transactionNext); + const proxyTxNextHash = await proxyProvider.sendTransaction(transactionNext); + + assert.equal(apiTxNextHash, proxyTxNextHash); + }); + it("should have the same response for transactions with events", async function () { const hash = "1b04eb849cf87f2d3086c77b4b825d126437b88014327bbf01437476751cb040"; diff --git a/src/networkProviders/transactions.ts b/src/networkProviders/transactions.ts index beaecc0a..7c208199 100644 --- a/src/networkProviders/transactions.ts +++ b/src/networkProviders/transactions.ts @@ -6,7 +6,7 @@ import { TransactionLogs } from "./transactionLogs"; import { TransactionReceipt } from "./transactionReceipt"; export function prepareTransactionForBroadcasting(transaction: ITransaction | ITransactionNext): any { - if ("toSendable" in transaction){ + if ("toSendable" in transaction) { return transaction.toSendable(); } @@ -15,8 +15,12 @@ export function prepareTransactionForBroadcasting(transaction: ITransaction | IT value: transaction.value.toString(), receiver: transaction.receiver, sender: transaction.sender, - senderUsername: transaction.senderUsername ? Buffer.from(transaction.senderUsername).toString("base64") : undefined, - receiverUsername: transaction.receiverUsername ? Buffer.from(transaction.receiverUsername).toString("base64") : undefined, + senderUsername: transaction.senderUsername + ? Buffer.from(transaction.senderUsername).toString("base64") + : undefined, + receiverUsername: transaction.receiverUsername + ? Buffer.from(transaction.receiverUsername).toString("base64") + : undefined, gasPrice: Number(transaction.gasPrice), gasLimit: Number(transaction.gasLimit), data: transaction.data.length === 0 ? undefined : Buffer.from(transaction.data).toString("base64"), @@ -25,8 +29,15 @@ export function prepareTransactionForBroadcasting(transaction: ITransaction | IT options: transaction.options, guardian: transaction.guardian || undefined, signature: Buffer.from(transaction.signature).toString("hex"), - guardianSignature: transaction.guardianSignature.length === 0 ? undefined : Buffer.from(transaction.guardianSignature).toString("hex"), - } + guardianSignature: + transaction.guardianSignature.length === 0 + ? undefined + : Buffer.from(transaction.guardianSignature).toString("hex"), + relayer: transaction.relayer ? transaction.relayer : undefined, + innerTransactions: transaction.innerTransactions + ? transaction.innerTransactions.map((tx) => prepareTransactionForBroadcasting(tx)) + : undefined, + }; } export class TransactionOnNetwork { @@ -54,18 +65,23 @@ export class TransactionOnNetwork { receipt: TransactionReceipt = new TransactionReceipt(); contractResults: ContractResults = new ContractResults([]); logs: TransactionLogs = new TransactionLogs(); + innerTransactions: ITransactionNext[] = []; constructor(init?: Partial) { Object.assign(this, init); } - static fromProxyHttpResponse(txHash: string, response: any, processStatus?: TransactionStatus | undefined): TransactionOnNetwork { + static fromProxyHttpResponse( + txHash: string, + response: any, + processStatus?: TransactionStatus | undefined, + ): TransactionOnNetwork { let result = TransactionOnNetwork.fromHttpResponse(txHash, response); result.contractResults = ContractResults.fromProxyHttpResponse(response.smartContractResults || []); if (processStatus) { result.status = processStatus; - result.isCompleted = result.status.isSuccessful() || result.status.isFailed() + result.isCompleted = result.status.isSuccessful() || result.status.isFailed(); } return result; @@ -102,10 +118,35 @@ export class TransactionOnNetwork { result.receipt = TransactionReceipt.fromHttpResponse(response.receipt || {}); result.logs = TransactionLogs.fromHttpResponse(response.logs || {}); + result.innerTransactions = (response.innerTransactions || []).map(this.innerTransactionFromHttpResource); return result; } + private static innerTransactionFromHttpResource(resource: any): ITransactionNext { + return { + nonce: BigInt(resource.nonce || 0), + value: BigInt(resource.value || 0), + receiver: resource.receiver, + sender: resource.sender, + // We discard "senderUsername" and "receiverUsername" (to avoid future discrepancies between Proxy and API): + senderUsername: "", + receiverUsername: "", + gasPrice: BigInt(resource.gasPrice), + gasLimit: BigInt(resource.gasLimit), + data: Buffer.from(resource.data || "", "base64"), + chainID: resource.chainID, + version: resource.version, + options: resource.options || 0, + guardian: resource.guardian || "", + signature: Buffer.from(resource.signature, "hex"), + guardianSignature: resource.guardianSignature + ? Buffer.from(resource.guardianSignature, "hex") + : Buffer.from([]), + relayer: resource.relayer, + }; + } + getDateTime(): Date { return new Date(this.timestamp * 1000); }