From 1115fce77b65432d2c976280584782d98f8a2a49 Mon Sep 17 00:00:00 2001 From: Satya Vusirikala Date: Thu, 9 Jan 2025 21:04:09 -0800 Subject: [PATCH] Support orderless transactions in SDK --- src/internal/transactionSubmission.ts | 9 +- .../instances/transactionPayload.ts | 288 ++++++++++++++++-- .../management/transactionWorker.ts | 93 ++++-- .../transactionBuilder/transactionBuilder.ts | 105 ++++--- src/transactions/types.ts | 14 +- src/types/types.ts | 15 +- 6 files changed, 436 insertions(+), 88 deletions(-) diff --git a/src/internal/transactionSubmission.ts b/src/internal/transactionSubmission.ts index 16def0e5a..75b8ceee0 100644 --- a/src/internal/transactionSubmission.ts +++ b/src/internal/transactionSubmission.ts @@ -113,14 +113,15 @@ export async function generateTransaction( export async function buildTransactionPayload( args: { aptosConfig: AptosConfig } & InputGenerateTransactionData, ): Promise { - const { aptosConfig, data } = args; + const { aptosConfig, data, options } = args; + // Merge in aptosConfig for remote ABI on non-script payloads let generateTransactionPayloadData: InputGenerateTransactionPayloadDataWithRemoteABI; let payload: AnyTransactionPayloadInstance; if ("bytecode" in data) { // TODO: Add ABI checking later - payload = await generateTransactionPayload(data); + payload = await generateTransactionPayload(data, options); } else if ("multisigAddress" in data) { generateTransactionPayloadData = { aptosConfig, @@ -130,7 +131,7 @@ export async function buildTransactionPayload( typeArguments: data.typeArguments, abi: data.abi, }; - payload = await generateTransactionPayload(generateTransactionPayloadData); + payload = await generateTransactionPayload(generateTransactionPayloadData, options); } else { generateTransactionPayloadData = { aptosConfig, @@ -139,7 +140,7 @@ export async function buildTransactionPayload( typeArguments: data.typeArguments, abi: data.abi, }; - payload = await generateTransactionPayload(generateTransactionPayloadData); + payload = await generateTransactionPayload(generateTransactionPayloadData, options); } return payload; } diff --git a/src/transactions/instances/transactionPayload.ts b/src/transactions/instances/transactionPayload.ts index 4319cc7d3..195ae9bc8 100644 --- a/src/transactions/instances/transactionPayload.ts +++ b/src/transactions/instances/transactionPayload.ts @@ -12,7 +12,7 @@ import { AccountAddress } from "../../core"; import { Identifier } from "./identifier"; import { ModuleId } from "./moduleId"; import type { EntryFunctionArgument, ScriptFunctionArgument, TransactionArgument } from "./transactionArgument"; -import { MoveModuleId, ScriptTransactionArgumentVariants, TransactionPayloadVariants } from "../../types"; +import { MoveModuleId, ScriptTransactionArgumentVariants, TransactionPayloadVariants, TransactionExecutableVariants, TransactionExtraConfigVariants } from "../../types"; import { TypeTag } from "../typeTag"; /** @@ -95,6 +95,8 @@ export abstract class TransactionPayload extends Serializable { return TransactionPayloadEntryFunction.load(deserializer); case TransactionPayloadVariants.Multisig: return TransactionPayloadMultiSig.load(deserializer); + case TransactionPayloadVariants.Payload: + return TransactionPayloadInner.load(deserializer); default: throw new Error(`Unknown variant index for TransactionPayload: ${index}`); } @@ -113,14 +115,6 @@ export abstract class TransactionPayload extends Serializable { export class TransactionPayloadScript extends TransactionPayload { public readonly script: Script; - /** - * Initializes a multi-sig account transaction with the provided payload. - * - * @param script - The payload of the multi-sig transaction. This can only be an EntryFunction for now, but Script might be - * supported in the future. - * @group Implementation - * @category Transactions - */ constructor(script: Script) { super(); this.script = script; @@ -139,8 +133,8 @@ export class TransactionPayloadScript extends TransactionPayload { } /** - * Loads a MultiSig transaction payload from the provided deserializer. - * This function helps in reconstructing a MultiSig transaction payload from its serialized form. + * Loads a script transaction payload from the provided deserializer. + * This function helps in reconstructing a script transaction payload from its serialized form. * * @param deserializer - The deserializer used to read the serialized data. * @group Implementation @@ -202,6 +196,264 @@ export class TransactionPayloadMultiSig extends TransactionPayload { } } +export class TransactionPayloadInner extends TransactionPayload { + public readonly executable: TransactionExecutable; + public readonly extra_config: TransactionExtraConfig; + + constructor(executable: TransactionExecutable, extra_config: TransactionExtraConfig) { + super(); + this.executable = executable; + this.extra_config = extra_config; + } + + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionPayloadVariants.Payload); + this.executable.serialize(serializer); + this.extra_config.serialize(serializer); + } + + static load(deserializer: Deserializer): TransactionPayloadInner { + const executable = TransactionExecutable.deserialize(deserializer); + const extra_config = TransactionExtraConfig.deserialize(deserializer); + return new TransactionPayloadInner(executable, extra_config); + } +} + +/** + * Represents a supported Transaction Executable that can be serialized and deserialized. + * + * This class serves as a base for different types of transaction payloads, allowing for + * their serialization into a format suitable for transmission and deserialization back + * into their original form. + * @group Implementation + * @category Transactions + */ +export abstract class TransactionExecutable extends Serializable { + /** + * Serialize a Transaction Payload + * @group Implementation + * @category Transactions + */ + abstract serialize(serializer: Serializer): void; + + /** + * Deserialize a Transaction Payload + * @param deserializer - The deserializer instance used to read the serialized data. + * @group Implementation + * @category Transactions + */ + static deserialize(deserializer: Deserializer): TransactionExecutable { + // index enum variant + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionExecutableVariants.Script: + return TransactionExecutableScript.load(deserializer); + case TransactionExecutableVariants.EntryFunction: + return TransactionExecutableEntryFunction.load(deserializer); + case TransactionExecutableVariants.Empty: + return TransactionExecutableEmpty.load(deserializer); + default: + throw new Error(`Unknown variant index for TransactionExecutable: ${index}`); + } + } +} + +/** + * Represents a transaction executable script that can be serialized and deserialized. + * + * This class encapsulates a script that defines the logic for a transaction executable. + * + * @extends TransactionExecutable + * @group Implementation + * @category Transactions + */ +export class TransactionExecutableScript extends TransactionExecutable { + public readonly script: Script; + + constructor(script: Script) { + super(); + this.script = script; + } + + /** + * Serializes the transaction executable, enabling future support for multiple types of inner transaction executables. + * + * @param serializer - The serializer instance used to serialize the transaction data. + * @group Implementation + * @category Transactions + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExecutableVariants.Script); + this.script.serialize(serializer); + } + + /** + * Loads a script transaction executable from the provided deserializer. + * This function helps in reconstructing a script transaction executable from its serialized form. + * + * @param deserializer - The deserializer used to read the serialized data. + * @group Implementation + * @category Transactions + */ + static load(deserializer: Deserializer): TransactionExecutableScript { + const script = Script.deserialize(deserializer); + return new TransactionExecutableScript(script); + } +} + +/** + * Represents a transaction executable entry function that can be serialized and deserialized. + * + * @extends TransactionExecutable + * @group Implementation + * @category Transactions + */ +export class TransactionExecutableEntryFunction extends TransactionExecutable { + public readonly entryFunction: EntryFunction; + + constructor(entryFunction: EntryFunction) { + super(); + this.entryFunction = entryFunction; + } + + /** + * Serializes the transaction executable, enabling future support for multiple types of inner transaction executables. + * + * @param serializer - The serializer instance used to serialize the transaction data. + * @group Implementation + * @category Transactions + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExecutableVariants.EntryFunction); + this.entryFunction.serialize(serializer); + } + + /** + * Loads a entry function transaction executable from the provided deserializer. + * This function helps in reconstructing a entry function transaction executable from its serialized form. + * + * @param deserializer - The deserializer used to read the serialized data. + * @group Implementation + * @category Transactions + */ + static load(deserializer: Deserializer): TransactionExecutableEntryFunction { + const entryFunction = EntryFunction.deserialize(deserializer); + return new TransactionExecutableEntryFunction(entryFunction); + } +} + + +/** + * Represents a transaction executable entry function that can be serialized and deserialized. + * + * @extends TransactionExecutable + * @group Implementation + * @category Transactions + */ +export class TransactionExecutableEmpty extends TransactionExecutable { + + constructor() { + super(); + } + + /** + * Serializes the transaction executable, enabling future support for multiple types of inner transaction executables. + * + * @param serializer - The serializer instance used to serialize the transaction data. + * @group Implementation + * @category Transactions + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExecutableVariants.Empty); + } + + /** + * Loads a empty transaction executable from the provided deserializer. + * This function helps in reconstructing a empty transaction executable from its serialized form. + * + * @param deserializer - The deserializer used to read the serialized data. + * @group Implementation + * @category Transactions + */ + static load(deserializer: Deserializer): TransactionExecutableEmpty { + return new TransactionExecutableEmpty(); + } +} + + +/** + * Represents a supported TransactionExtraConfig that can be serialized and deserialized. + * + * This class serves as a base for different types of transaction extra configs, allowing for + * their serialization into a format suitable for transmission and deserialization back + * into their original form. + * @group Implementation + * @category Transactions + */ +export abstract class TransactionExtraConfig extends Serializable { + /** + * Serialize a Transaction Extra Config + * @group Implementation + * @category Transactions + */ + abstract serialize(serializer: Serializer): void; + + /** + * Deserialize a Transaction Extra Config + * @param deserializer - The deserializer instance used to read the serialized data. + * @group Implementation + * @category Transactions + */ + static deserialize(deserializer: Deserializer): TransactionExtraConfig { + // index enum variant + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionExtraConfigVariants.V1: + return TransactionExtraConfigV1.load(deserializer); + default: + throw new Error(`Unknown variant index for TransactionExtraConfig: ${index}`); + } + } +} + + +export class TransactionExtraConfigV1 extends TransactionExtraConfig { + public readonly multisigAddress: AccountAddress | undefined; + // Question: Is it correct to define replayProtectionNonce as a U64? + public readonly replayProtectionNonce: bigint | undefined; + // Question: How to make these fields optional? + constructor(multisigAddress?: AccountAddress, replayProtectionNonce?: bigint) { + super(); + this.multisigAddress = multisigAddress; + this.replayProtectionNonce = replayProtectionNonce; + } + + serialize(serializer: Serializer): void { + // TODO: How to serialize these optional fields? + if (this.multisigAddress) { + this.multisigAddress.serialize(serializer); + } + if (this.replayProtectionNonce) { + serializer.serializeU64(this.replayProtectionNonce); + } + } + + static load(deserializer: Deserializer): TransactionExtraConfigV1 { + // TODO: How to serialize these optional fields? + const hasMultisigAddress = deserializer.deserializeBool(); + let multisigAddress; + if (hasMultisigAddress) { + multisigAddress = AccountAddress.deserialize(deserializer); + } + const hasReplayProtectionNonce = deserializer.deserializeBool(); + let replayProtectionNonce; + if (hasReplayProtectionNonce) { + replayProtectionNonce = deserializer.deserializeU64(); + } + return new TransactionExtraConfigV1(multisigAddress, replayProtectionNonce); + } +} + /** * Represents an entry function that can be serialized and deserialized. * This class encapsulates the details required to invoke a function within a module, @@ -425,27 +677,27 @@ export class Script { * @category Transactions */ export class MultiSig { - public readonly multisig_address: AccountAddress; + public readonly multisigAddress: AccountAddress; public readonly transaction_payload?: MultiSigTransactionPayload; /** * Contains the payload to run a multi-sig account transaction. * - * @param multisig_address The multi-sig account address the transaction will be executed as. + * @param multisigAddress The multi-sig account address the transaction will be executed as. * * @param transaction_payload The payload of the multi-sig transaction. This is optional when executing a multi-sig * transaction whose payload is already stored on chain. * @group Implementation * @category Transactions */ - constructor(multisig_address: AccountAddress, transaction_payload?: MultiSigTransactionPayload) { - this.multisig_address = multisig_address; + constructor(multisigAddress: AccountAddress, transaction_payload?: MultiSigTransactionPayload) { + this.multisigAddress = multisigAddress; this.transaction_payload = transaction_payload; } serialize(serializer: Serializer): void { - this.multisig_address.serialize(serializer); + this.multisigAddress.serialize(serializer); // Options are encoded with an extra u8 field before the value - 0x0 is none and 0x1 is present. // We use serializeBool below to create this prefix value. if (this.transaction_payload === undefined) { @@ -457,13 +709,13 @@ export class MultiSig { } static deserialize(deserializer: Deserializer): MultiSig { - const multisig_address = AccountAddress.deserialize(deserializer); + const multisigAddress = AccountAddress.deserialize(deserializer); const payloadPresent = deserializer.deserializeBool(); let transaction_payload; if (payloadPresent) { transaction_payload = MultiSigTransactionPayload.deserialize(deserializer); } - return new MultiSig(multisig_address, transaction_payload); + return new MultiSig(multisigAddress, transaction_payload); } } diff --git a/src/transactions/management/transactionWorker.ts b/src/transactions/management/transactionWorker.ts index 0e1bd6c48..8c24115c4 100644 --- a/src/transactions/management/transactionWorker.ts +++ b/src/transactions/management/transactionWorker.ts @@ -77,6 +77,10 @@ export type FailureEventData = { error: string; }; +type ReplayProtector = + | { type: "SequenceNumber"; value: bigint } + | { type: "Nonce"; value: bigint }; + /** * TransactionWorker provides a simple framework for receiving payloads to be processed. * @@ -97,8 +101,8 @@ export class TransactionWorker extends EventEmitter { readonly account: Account; // current account sequence number - // TODO: Rename Sequnce -> Sequence - readonly accountSequnceNumber: AccountSequenceNumber; + // Question: Is it okay to rename accountSequnceNumber --> accountSequenceNumber? + readonly accountSequenceNumber: AccountSequenceNumber; readonly taskQueue: AsyncQueue<() => Promise> = new AsyncQueue<() => Promise>(); @@ -121,21 +125,21 @@ export class TransactionWorker extends EventEmitter { * @group Implementation * @category Transactions */ - outstandingTransactions = new AsyncQueue<[Promise, bigint]>(); + outstandingTransactions = new AsyncQueue<[Promise, ReplayProtector]>(); /** * transactions that have been submitted to chain * @group Implementation * @category Transactions */ - sentTransactions: Array<[string, bigint, any]> = []; + sentTransactions: Array<[string, ReplayProtector, any]> = []; /** * transactions that have been committed to chain * @group Implementation * @category Transactions */ - executedTransactions: Array<[string, bigint, any]> = []; + executedTransactions: Array<[string, ReplayProtector, any]> = []; /** * Initializes a new instance of the class, providing a framework for receiving payloads to be processed. @@ -161,7 +165,7 @@ export class TransactionWorker extends EventEmitter { this.aptosConfig = aptosConfig; this.account = account; this.started = false; - this.accountSequnceNumber = new AccountSequenceNumber( + this.accountSequenceNumber = new AccountSequenceNumber( aptosConfig, account, maxWaitTime, @@ -183,16 +187,42 @@ export class TransactionWorker extends EventEmitter { try { /* eslint-disable no-constant-condition */ while (true) { - const sequenceNumber = await this.accountSequnceNumber.nextSequenceNumber(); - if (sequenceNumber === null) return; - const transaction = await this.generateNextTransaction(this.account, sequenceNumber); - if (!transaction) return; - const pendingTransaction = signAndSubmitTransaction({ - aptosConfig: this.aptosConfig, - transaction, - signer: this.account, - }); - await this.outstandingTransactions.enqueue([pendingTransaction, sequenceNumber]); + if (this.transactionsQueue.isEmpty()) return; + const [transactionData, options] = await this.transactionsQueue.dequeue(); + let { replayProtectionNonce } = options ?? {}; + if (replayProtectionNonce) { + // Generate orderless transaction with nonce + const transaction = await generateTransaction({ + aptosConfig: this.aptosConfig, + sender: this.account.accountAddress, + data: transactionData, + options, + }); + if (!transaction) return; + const pendingTransaction = signAndSubmitTransaction({ + aptosConfig: this.aptosConfig, + transaction, + signer: this.account, + }); + await this.outstandingTransactions.enqueue([pendingTransaction, { type: "Nonce", value: replayProtectionNonce }]); + } else { + // Generate sequence number based transaction + const sequenceNumber = await this.accountSequenceNumber.nextSequenceNumber(); + if (sequenceNumber === null) return; + const transaction = await generateTransaction({ + aptosConfig: this.aptosConfig, + sender: this.account.accountAddress, + data: transactionData, + options: { ...options, accountSequenceNumber: sequenceNumber }, + }) + if (!transaction) return; + const pendingTransaction = signAndSubmitTransaction({ + aptosConfig: this.aptosConfig, + transaction, + signer: this.account, + }); + await this.outstandingTransactions.enqueue([pendingTransaction, { type: "SequenceNumber", value: sequenceNumber }]); + } } } catch (error: any) { if (error instanceof AsyncQueueCancelledError) { @@ -220,36 +250,36 @@ export class TransactionWorker extends EventEmitter { /* eslint-disable no-constant-condition */ while (true) { const awaitingTransactions = []; - const sequenceNumbers = []; - let [pendingTransaction, sequenceNumber] = await this.outstandingTransactions.dequeue(); + const replayProtectors = []; + let [pendingTransaction, replayProtector] = await this.outstandingTransactions.dequeue(); awaitingTransactions.push(pendingTransaction); - sequenceNumbers.push(sequenceNumber); + replayProtectors.push(replayProtector); while (!this.outstandingTransactions.isEmpty()) { - [pendingTransaction, sequenceNumber] = await this.outstandingTransactions.dequeue(); + [pendingTransaction, replayProtector] = await this.outstandingTransactions.dequeue(); awaitingTransactions.push(pendingTransaction); - sequenceNumbers.push(sequenceNumber); + replayProtectors.push(replayProtector); } // send awaiting transactions to chain const sentTransactions = await Promise.allSettled(awaitingTransactions); - for (let i = 0; i < sentTransactions.length && i < sequenceNumbers.length; i += 1) { + for (let i = 0; i < sentTransactions.length && i < replayProtectors.length; i += 1) { // check sent transaction status const sentTransaction = sentTransactions[i]; - sequenceNumber = sequenceNumbers[i]; + replayProtector = replayProtectors[i]; if (sentTransaction.status === promiseFulfilledStatus) { // transaction sent to chain - this.sentTransactions.push([sentTransaction.value.hash, sequenceNumber, null]); + this.sentTransactions.push([sentTransaction.value.hash, replayProtector, null]); // check sent transaction execution this.emit(TransactionWorkerEventsEnum.TransactionSent, { message: `transaction hash ${sentTransaction.value.hash} has been committed to chain`, transactionHash: sentTransaction.value.hash, }); - await this.checkTransaction(sentTransaction, sequenceNumber); + await this.checkTransaction(sentTransaction, replayProtector); } else { // send transaction failed - this.sentTransactions.push([sentTransaction.status, sequenceNumber, sentTransaction.reason]); + this.sentTransactions.push([sentTransaction.status, replayProtector, sentTransaction.reason]); this.emit(TransactionWorkerEventsEnum.TransactionSendFailed, { message: `failed to commit transaction ${this.sentTransactions.length} with error ${sentTransaction.reason}`, error: sentTransaction.reason, @@ -275,7 +305,9 @@ export class TransactionWorker extends EventEmitter { * @group Implementation * @category Transactions */ - async checkTransaction(sentTransaction: PromiseFulfilledResult, sequenceNumber: bigint) { + + // Question: Is it okay to change this function signature to take replayProtector instead of sequenceNumber? + async checkTransaction(sentTransaction: PromiseFulfilledResult, replayProtector: ReplayProtector) { try { const waitFor: Array> = []; waitFor.push(waitForTransaction({ aptosConfig: this.aptosConfig, transactionHash: sentTransaction.value.hash })); @@ -285,14 +317,14 @@ export class TransactionWorker extends EventEmitter { const executedTransaction = sentTransactions[i]; if (executedTransaction.status === promiseFulfilledStatus) { // transaction executed to chain - this.executedTransactions.push([executedTransaction.value.hash, sequenceNumber, null]); + this.executedTransactions.push([executedTransaction.value.hash, replayProtector, null]); this.emit(TransactionWorkerEventsEnum.TransactionExecuted, { message: `transaction hash ${executedTransaction.value.hash} has been executed on chain`, transactionHash: sentTransaction.value.hash, }); } else { // transaction execution failed - this.executedTransactions.push([executedTransaction.status, sequenceNumber, executedTransaction.reason]); + this.executedTransactions.push([executedTransaction.status, replayProtector, executedTransaction.reason]); this.emit(TransactionWorkerEventsEnum.TransactionExecutionFailed, { message: `failed to execute transaction ${this.executedTransactions.length} with error ${executedTransaction.reason}`, error: executedTransaction.reason, @@ -324,9 +356,10 @@ export class TransactionWorker extends EventEmitter { this.transactionsQueue.enqueue([transactionData, options]); } + // Question: Can we deprecate this function? /** * Generates a signed transaction that can be submitted to the chain. - * + * @deprecated * @param account - An Aptos account used as the sender of the transaction. * @param sequenceNumber - A sequence number the transaction will be generated with. * @returns A signed transaction object or undefined if the transaction queue is empty. diff --git a/src/transactions/transactionBuilder/transactionBuilder.ts b/src/transactions/transactionBuilder/transactionBuilder.ts index 5527302d2..71373fa07 100644 --- a/src/transactions/transactionBuilder/transactionBuilder.ts +++ b/src/transactions/transactionBuilder/transactionBuilder.ts @@ -49,9 +49,13 @@ import { MultiSigTransactionPayload, RawTransaction, Script, + TransactionExecutableScript, TransactionPayloadEntryFunction, + TransactionPayloadInner, TransactionPayloadMultiSig, TransactionPayloadScript, + TransactionExtraConfigV1, + TransactionExecutableEntryFunction, } from "../instances"; import { SignedTransaction } from "../instances/signedTransaction"; import { @@ -98,21 +102,29 @@ import { MultiAgentTransaction } from "../instances/multiAgentTransaction"; * @group Implementation * @category Transactions */ -export async function generateTransactionPayload(args: InputScriptData): Promise; +// Question: Is it correct to define the return type as TransactionPayloadScript | TransactionPayloadInner? +export async function generateTransactionPayload( + args: InputScriptData, + options?: InputGenerateTransactionOptions +): Promise; /** * @group Implementation * @category Transactions */ +// Question: Is it correct to define the return type as TransactionPayloadEntryFunction | TransactionPayloadInner? export async function generateTransactionPayload( args: InputEntryFunctionDataWithRemoteABI, -): Promise; + options?: InputGenerateTransactionOptions +): Promise; /** * @group Implementation * @category Transactions */ +// Question: Is it correct to define the return type as TransactionPayloadMultiSig | TransactionPayloadInner? export async function generateTransactionPayload( args: InputMultiSigDataWithRemoteABI, -): Promise; + options?: InputGenerateTransactionOptions, +): Promise; /** * Builds a transaction payload based on the data argument and returns @@ -128,24 +140,36 @@ export async function generateTransactionPayload( */ export async function generateTransactionPayload( args: InputGenerateTransactionPayloadDataWithRemoteABI, + options?: InputGenerateTransactionOptions, ): Promise { + let { replayProtectionNonce, useTransactionPayloadV2 } = options ?? {}; if (isScriptDataInput(args)) { - return generateTransactionPayloadScript(args); + let script = generateScript(args); + // If nonce is provided or if useTransactionPayloadV2 is true, we need to use the new format + if (replayProtectionNonce || useTransactionPayloadV2) { + let executable = new TransactionExecutableScript(script); + let extraConfig = new TransactionExtraConfigV1(undefined, replayProtectionNonce); + return new TransactionPayloadInner(executable, extraConfig); + } else { + return new TransactionPayloadScript(generateScript(args)); + } + } + else { + const { moduleAddress, moduleName, functionName } = getFunctionParts(args.function); + + const functionAbi = await fetchAbi({ + key: "entry-function", + moduleAddress, + moduleName, + functionName, + aptosConfig: args.aptosConfig, + abi: args.abi, + fetch: fetchEntryFunctionAbi, + }); + + // Fill in the ABI + return generateTransactionPayloadWithABI({ ...args, abi: functionAbi }, options); } - const { moduleAddress, moduleName, functionName } = getFunctionParts(args.function); - - const functionAbi = await fetchAbi({ - key: "entry-function", - moduleAddress, - moduleName, - functionName, - aptosConfig: args.aptosConfig, - abi: args.abi, - fetch: fetchEntryFunctionAbi, - }); - - // Fill in the ABI - return generateTransactionPayloadWithABI({ ...args, abi: functionAbi }); } /** @@ -163,18 +187,21 @@ export async function generateTransactionPayload( * @group Implementation * @category Transactions */ -export function generateTransactionPayloadWithABI(args: InputEntryFunctionDataWithABI): TransactionPayloadEntryFunction; +// Question: Is it correct to define the return type as TransactionPayloadEntryFunction | TransactionPayloadInner? +export function generateTransactionPayloadWithABI(args: InputEntryFunctionDataWithABI, options?: InputGenerateTransactionOptions): TransactionPayloadEntryFunction | TransactionPayloadInner; /** * @group Implementation * @category Transactions */ -export function generateTransactionPayloadWithABI(args: InputMultiSigDataWithABI): TransactionPayloadMultiSig; +// Question: Is it correct to define the return type as TransactionPayloadMultiSig | TransactionPayloadInner? +export function generateTransactionPayloadWithABI(args: InputMultiSigDataWithABI, options?: InputGenerateTransactionOptions): TransactionPayloadMultiSig | TransactionPayloadInner; /** * @group Implementation * @category Transactions */ export function generateTransactionPayloadWithABI( args: InputGenerateTransactionPayloadDataWithABI, + options?: InputGenerateTransactionOptions, ): AnyTransactionPayloadInstance { const functionAbi = args.abi; const { moduleAddress, moduleName, functionName } = getFunctionParts(args.function); @@ -224,16 +251,28 @@ export function generateTransactionPayloadWithABI( functionArguments, ); - // Send it as multi sig if it's a multisig payload - if ("multisigAddress" in args) { - const multisigAddress = AccountAddress.from(args.multisigAddress); - return new TransactionPayloadMultiSig( - new MultiSig(multisigAddress, new MultiSigTransactionPayload(entryFunctionPayload)), - ); - } + let { replayProtectionNonce, useTransactionPayloadV2 } = options ?? {}; + // If nonce is provided or if useTransactionPayloadV2 is true, we need to use the new payload format + if (replayProtectionNonce || useTransactionPayloadV2) { + let multisigAddress; + if ("multisigAddress" in args) { + multisigAddress = AccountAddress.from(args.multisigAddress); + } + let extraConfig = new TransactionExtraConfigV1(multisigAddress, replayProtectionNonce); + let executable = new TransactionExecutableEntryFunction(entryFunctionPayload); + return new TransactionPayloadInner(executable, extraConfig); + } else { + // Send it as multi sig if it's a multisig payload + if ("multisigAddress" in args) { + const multisigAddress = AccountAddress.from(args.multisigAddress); + return new TransactionPayloadMultiSig( + new MultiSig(multisigAddress, new MultiSigTransactionPayload(entryFunctionPayload)), + ); + } - // Otherwise send as an entry function - return new TransactionPayloadEntryFunction(entryFunctionPayload); + // Otherwise send as an entry function + return new TransactionPayloadEntryFunction(entryFunctionPayload); + } } /** @@ -324,14 +363,12 @@ export function generateViewFunctionPayloadWithABI(args: InputViewFunctionDataWi * @group Implementation * @category Transactions */ -function generateTransactionPayloadScript(args: InputScriptData) { - return new TransactionPayloadScript( - new Script( +function generateScript(args: InputScriptData) { + return new Script( Hex.fromHexInput(args.bytecode).toUint8Array(), standardizeTypeTags(args.typeArguments), args.functionArguments, - ), - ); + ); } /** diff --git a/src/transactions/types.ts b/src/transactions/types.ts index c95f9f654..84c2b312b 100644 --- a/src/transactions/types.ts +++ b/src/transactions/types.ts @@ -14,6 +14,7 @@ import { TransactionPayloadEntryFunction, TransactionPayloadMultiSig, TransactionPayloadScript, + TransactionPayloadInner, } from "./instances"; import { AnyNumber, HexInput, MoveFunctionGenericTypeParam, MoveFunctionId, MoveStructId, MoveValue } from "../types"; import { TypeTag } from "./typeTag"; @@ -118,6 +119,16 @@ export type InputGenerateTransactionOptions = { gasUnitPrice?: number; expireTimestamp?: number; accountSequenceNumber?: AnyNumber; + /** + * If nonce is provided, an orderless transaction will be generated. + * The `accountSequenceNumber` will be ignored, and the sequence number in the transaction will be hardcoded + * to u64::MAX. + */ + // Question: Is it correct to define replayProtectionNonce as a U64? + replayProtectionNonce?: bigint; + // Question: Do we need this flag? + // If true, the transaction payload will be generated with the new format. + useTransactionPayloadV2?: boolean; }; /** @@ -129,7 +140,8 @@ export type InputGenerateTransactionOptions = { export type AnyTransactionPayloadInstance = | TransactionPayloadEntryFunction | TransactionPayloadScript - | TransactionPayloadMultiSig; + | TransactionPayloadMultiSig + | TransactionPayloadInner; /** * The data needed to generate a transaction payload for Entry Function, Script, or Multi Sig types. diff --git a/src/types/types.ts b/src/types/types.ts index a0a733047..c9a590db9 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -73,6 +73,17 @@ export enum TransactionPayloadVariants { Script = 0, EntryFunction = 2, Multisig = 3, + Payload = 4, +} + +export enum TransactionExecutableVariants { + Script = 0, + EntryFunction = 1, + Empty = 2, +} + +export enum TransactionExtraConfigVariants { + V1 = 0, } /** @@ -554,6 +565,7 @@ export type PendingTransactionResponse = { expiration_timestamp_secs: string; payload: TransactionPayloadResponse; signature?: TransactionSignature; + replay_protection_nonce?: string; }; /** @@ -587,6 +599,7 @@ export type UserTransactionResponse = { expiration_timestamp_secs: string; payload: TransactionPayloadResponse; signature?: TransactionSignature; + replay_protection_nonce?: string; /** * Events generated by the transaction */ @@ -923,7 +936,7 @@ export type ScriptPayloadResponse = { */ export type MultisigPayloadResponse = { type: string; - multisig_address: string; + multisigAddress: string; transaction_payload?: EntryFunctionPayloadResponse; };