@aptos-labs/ts-sdk
Version:
Aptos TypeScript SDK
538 lines (501 loc) • 21.3 kB
text/typescript
/**
* This file contains the underlying implementations for exposed submission API surface in
* the {@link api/transaction}. By moving the methods out into a separate file,
* other namespaces and processes can access these methods without depending on the entire
* transaction namespace and without having a dependency cycle error.
* @group Implementation
*/
import { AptosConfig } from "../api/aptosConfig.js";
import { Deserializer, MoveVector } from "../bcs/index.js";
import { postAptosFullNode } from "../client/index.js";
import { Account } from "../account/index.js";
import { isKeylessSigner } from "../account/keylessSigner.js";
import { AccountAddress, AccountAddressInput } from "../core/accountAddress.js";
import { AccountAuthenticator } from "../transactions/authenticator/account.js";
import {
buildTransaction,
generateTransactionPayload,
generateSignedTransactionForSimulation,
generateSignedTransaction,
} from "../transactions/transactionBuilder/transactionBuilder.js";
import {
InputGenerateTransactionData,
AnyRawTransaction,
InputSimulateTransactionData,
InputGenerateTransactionOptions,
InputGenerateTransactionPayloadDataWithRemoteABI,
InputSubmitTransactionData,
InputGenerateMultiAgentRawTransactionData,
InputGenerateSingleSignerRawTransactionData,
AnyTransactionPayloadInstance,
EntryFunctionABI,
InputTransactionPluginData,
} from "../transactions/types.js";
import {
UserTransactionResponse,
PendingTransactionResponse,
MimeType,
HexInput,
AnyPublicKeyVariant,
} from "../types/index.js";
import { SignedTransaction, TypeTagVector, generateSigningMessageForTransaction } from "../transactions/index.js";
import { SimpleTransaction } from "../transactions/instances/simpleTransaction.js";
import { MultiAgentTransaction } from "../transactions/instances/multiAgentTransaction.js";
import { TransactionPayloadEncryptedPayload } from "../transactions/instances/transactionPayload.js";
/**
* We are defining function signatures, each with its specific input and output.
* These are the possible function signature for `generateTransaction` function.
* When we call `generateTransaction` function with the relevant type properties,
* Typescript can infer the return type based on the appropriate function overload.
* @group Implementation
*/
export async function generateTransaction(
args: { aptosConfig: AptosConfig } & InputGenerateSingleSignerRawTransactionData,
): Promise<SimpleTransaction>;
export async function generateTransaction(
args: { aptosConfig: AptosConfig } & InputGenerateMultiAgentRawTransactionData,
): Promise<MultiAgentTransaction>;
/**
* Generates any transaction by passing in the required arguments
*
* @param args.sender The transaction sender's account address as a AccountAddressInput
* @param args.data EntryFunctionData | ScriptData | MultiSigData
* @param args.feePayerAddress optional. For a fee payer (aka sponsored) transaction
* @param args.secondarySignerAddresses optional. For a multi-agent or fee payer (aka sponsored) transactions
* @param args.options optional. GenerateTransactionOptions type
*
* @example
* For a single signer entry function
* move function name, move function type arguments, move function arguments
* `
* data: {
* function:"0x1::aptos_account::transfer",
* typeArguments:[]
* functionArguments :[receiverAddress,10]
* }
* `
*
* @example
* For a single signer script function
* module bytecode, move function type arguments, move function arguments
* ```
* data: {
* bytecode:"0x001234567",
* typeArguments:[],
* functionArguments :[receiverAddress,10]
* }
* ```
*
* @return An instance of a RawTransaction, plus optional secondary/fee payer addresses
* ```
* {
* rawTransaction: RawTransaction,
* secondarySignerAddresses?: Array<AccountAddress>,
* feePayerAddress?: AccountAddress
* }
* ```
* @group Implementation
*/
export async function generateTransaction(
args: { aptosConfig: AptosConfig } & InputGenerateTransactionData,
): Promise<AnyRawTransaction> {
const payload = await buildTransactionPayload(args);
return buildRawTransaction(args, payload);
}
/**
* Builds a transaction payload based on the provided configuration and input data.
* This function is essential for preparing transaction data for execution on the Aptos blockchain.
*
* @param args - The arguments for building the transaction payload.
* @param args.aptosConfig - Configuration settings for the Aptos network.
* @param args.data - Input data required to generate the transaction payload, which may include bytecode, multisig address,
* function name, function arguments, type arguments, and ABI.
* @returns A promise that resolves to the generated transaction payload instance.
* @group Implementation
*/
export async function buildTransactionPayload(
args: { aptosConfig: AptosConfig } & InputGenerateTransactionData,
): Promise<AnyTransactionPayloadInstance> {
const { aptosConfig, data } = 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);
} else if ("multisigAddress" in data) {
generateTransactionPayloadData = {
aptosConfig,
multisigAddress: data.multisigAddress,
function: data.function,
functionArguments: data.functionArguments,
typeArguments: data.typeArguments,
abi: data.abi,
};
payload = await generateTransactionPayload(generateTransactionPayloadData);
} else {
generateTransactionPayloadData = {
aptosConfig,
function: data.function,
functionArguments: data.functionArguments,
typeArguments: data.typeArguments,
abi: data.abi,
};
payload = await generateTransactionPayload(generateTransactionPayloadData);
}
return payload;
}
/**
* Builds a raw transaction based on the provided configuration and payload.
* This function helps in creating a transaction that can be sent to the Aptos blockchain.
*
* @param args - The arguments for generating the transaction.
* @param args.aptosConfig - The configuration settings for Aptos.
* @param args.sender - The address of the sender of the transaction.
* @param args.options - Additional options for the transaction.
* @param payload - The payload of the transaction, which defines the action to be performed.
* @group Implementation
*/
export async function buildRawTransaction(
args: { aptosConfig: AptosConfig } & InputGenerateTransactionData,
payload: AnyTransactionPayloadInstance,
): Promise<AnyRawTransaction> {
const { aptosConfig, sender, options } = args;
let feePayerAddress;
if (isFeePayerTransactionInput(args)) {
feePayerAddress = AccountAddress.ZERO.toString();
}
if (isMultiAgentTransactionInput(args)) {
const { secondarySignerAddresses } = args;
return buildTransaction({
aptosConfig,
sender,
payload,
options,
secondarySignerAddresses,
feePayerAddress,
});
}
return buildTransaction({
aptosConfig,
sender,
payload,
options,
feePayerAddress,
});
}
/**
* Determine if the transaction input includes a fee payer.
*
* @param data - The input data for generating a transaction.
* @param data.withFeePayer - Indicates whether a fee payer is included in the transaction input.
* @returns A boolean value indicating if the transaction input has a fee payer.
* @group Implementation
*/
function isFeePayerTransactionInput(data: InputGenerateTransactionData): boolean {
return data.withFeePayer === true;
}
/**
* Determines whether the provided transaction input data includes multiple agent signatures.
*
* @param data - The transaction input data to evaluate.
* @param data.secondarySignerAddresses - An array of secondary signer addresses, indicating multiple agents.
* @group Implementation
*/
function isMultiAgentTransactionInput(
data: InputGenerateTransactionData,
): data is InputGenerateMultiAgentRawTransactionData {
return "secondarySignerAddresses" in data;
}
/**
* Builds a signing message that can be signed by external signers.
*
* Note: Please prefer using `signTransaction` unless signing outside the SDK.
*
* @param args - The arguments for generating the signing message.
* @param args.transaction - AnyRawTransaction, as generated by `generateTransaction()`.
*
* @returns The message to be signed.
* @group Implementation
*/
export function getSigningMessage(args: { transaction: AnyRawTransaction }): Uint8Array {
const { transaction } = args;
return generateSigningMessageForTransaction(transaction);
}
/**
* Sign a transaction that can later be submitted to the chain.
*
* @param args The arguments for signing the transaction.
* @param args.signer The signer account to sign the transaction.
* @param args.transaction An instance of a RawTransaction, plus optional secondary/fee payer addresses.
*
* @return The signer AccountAuthenticator.
* @group Implementation
*/
export function signTransaction(args: { signer: Account; transaction: AnyRawTransaction }): AccountAuthenticator {
const { signer, transaction } = args;
return signer.signTransactionWithAuthenticator(transaction);
}
export function signAsFeePayer(args: { signer: Account; transaction: AnyRawTransaction }): AccountAuthenticator {
const { signer, transaction } = args;
// if transaction doesn't hold a "feePayerAddress" prop it means
// this is not a fee payer transaction
if (!transaction.feePayerAddress) {
throw new Error(`Transaction ${transaction} is not a Fee Payer transaction`);
}
// Set the feePayerAddress to the signer account address
transaction.feePayerAddress = signer.accountAddress;
return signTransaction({
signer,
transaction,
});
}
/**
* Error thrown when {@link simulateTransaction} is called with an encrypted payload.
* Simulation is not supported for encrypted transactions; build the same entry function without
* `encrypted: true`, simulate that transaction, then build with encryption for submit.
* @group Implementation
*/
export const ENCRYPTED_TRANSACTION_SIMULATION_NOT_SUPPORTED_MESSAGE =
"Transaction simulation is not supported for encrypted payloads. Build a plaintext transaction with the same entry function and simulate it, then build with options.encrypted true for submission.";
/**
* Throws if the transaction cannot be simulated because its payload is encrypted.
* @group Implementation
*/
export function assertSimulatableTransaction(transaction: AnyRawTransaction): void {
if (transaction.rawTransaction.payload instanceof TransactionPayloadEncryptedPayload) {
throw new Error(ENCRYPTED_TRANSACTION_SIMULATION_NOT_SUPPORTED_MESSAGE);
}
}
/**
* Simulates a transaction before signing it to evaluate its potential outcome.
*
* @param args The arguments for simulating the transaction.
* @param args.aptosConfig The configuration for the Aptos network.
* @param args.transaction The raw transaction to simulate. Must not use an encrypted payload
* (`options.encrypted` when building); otherwise this function throws before contacting the node.
* @param args.signerPublicKey Optional. The signer public key.
* @param args.secondarySignersPublicKeys Optional. For when the transaction involves multiple signers.
* @param args.feePayerPublicKey Optional. For when the transaction is sponsored by a fee payer.
* @param args.options Optional. A configuration object to customize the simulation process.
* @param args.options.estimateGasUnitPrice Optional. Indicates whether to estimate the gas unit price.
* @param args.options.estimateMaxGasAmount Optional. Indicates whether to estimate the maximum gas amount.
* @param args.options.estimatePrioritizedGasUnitPrice Optional. Indicates whether to estimate the prioritized gas unit price.
*
* @throws Error if `transaction` has an encrypted payload (see {@link assertSimulatableTransaction}).
* @group Implementation
*/
export async function simulateTransaction(
args: { aptosConfig: AptosConfig } & InputSimulateTransactionData,
): Promise<Array<UserTransactionResponse>> {
const { aptosConfig, transaction, signerPublicKey, secondarySignersPublicKeys, feePayerPublicKey, options } = args;
assertSimulatableTransaction(transaction);
const signedTransaction = await generateSignedTransactionForSimulation({
transaction,
signerPublicKey,
secondarySignersPublicKeys,
feePayerPublicKey,
options,
});
const { data } = await postAptosFullNode<Uint8Array, Array<UserTransactionResponse>>({
aptosConfig,
body: signedTransaction,
path: "transactions/simulate",
params: {
estimate_gas_unit_price: args.options?.estimateGasUnitPrice ?? false,
estimate_max_gas_amount: args.options?.estimateMaxGasAmount ?? false,
estimate_prioritized_gas_unit_price: args.options?.estimatePrioritizedGasUnitPrice ?? false,
},
originMethod: "simulateTransaction",
contentType: MimeType.BCS_SIGNED_TRANSACTION,
});
// If the server's gas estimation produced a value below the network minimum,
// retry without estimation so the transaction's original max_gas_amount is used.
if (
args.options?.estimateMaxGasAmount &&
data.length > 0 &&
data[0].vm_status === "MAX_GAS_UNITS_BELOW_MIN_TRANSACTION_GAS_UNITS"
) {
const { data: retryData } = await postAptosFullNode<Uint8Array, Array<UserTransactionResponse>>({
aptosConfig,
body: signedTransaction,
path: "transactions/simulate",
params: {
estimate_gas_unit_price: args.options?.estimateGasUnitPrice ?? false,
estimate_max_gas_amount: false,
estimate_prioritized_gas_unit_price: args.options?.estimatePrioritizedGasUnitPrice ?? false,
},
originMethod: "simulateTransaction",
contentType: MimeType.BCS_SIGNED_TRANSACTION,
});
return retryData;
}
return data;
}
/**
* Submit a transaction to the Aptos blockchain.
*
* @param args - The arguments for submitting the transaction.
* @param args.aptosConfig - The configuration for connecting to the Aptos network.
* @param args.transaction - The Aptos transaction data to be submitted.
* @param args.senderAuthenticator - The account authenticator of the transaction sender.
* @param args.secondarySignerAuthenticators - Optional. Authenticators for additional signers in a multi-signer transaction.
*
* @returns PendingTransactionResponse - The response containing the status of the submitted transaction.
* @group Implementation
*/
export async function submitTransaction(
args: {
aptosConfig: AptosConfig;
} & InputSubmitTransactionData,
): Promise<PendingTransactionResponse> {
const { aptosConfig, transactionSubmitter } = args;
const maybeTransactionSubmitter =
transactionSubmitter === undefined ? aptosConfig.getTransactionSubmitter() : transactionSubmitter;
if (maybeTransactionSubmitter) {
return maybeTransactionSubmitter.submitTransaction(args);
}
const signedTransaction = generateSignedTransaction({ ...args });
try {
const { data } = await postAptosFullNode<Uint8Array, PendingTransactionResponse>({
aptosConfig,
body: signedTransaction,
path: "transactions",
originMethod: "submitTransaction",
contentType: MimeType.BCS_SIGNED_TRANSACTION,
});
return data;
} catch (e) {
// Best-effort diagnostic: if this was a keyless submission, refresh the
// JWK so the next attempt can succeed. Any failure here (deserialization,
// unregistered variant, network error while fetching JWKs, etc.) must
// NOT mask the original submission error `e`.
try {
const signedTxn = SignedTransaction.deserialize(new Deserializer(signedTransaction));
if (signedTxn.authenticator.isSingleSender() && signedTxn.authenticator.sender.isSingleKey()) {
const { variant } = signedTxn.authenticator.sender.public_key;
if (variant === AnyPublicKeyVariant.Keyless || variant === AnyPublicKeyVariant.FederatedKeyless) {
// Dynamic import to avoid pulling poseidon-lite into the main bundle
const { AbstractKeylessAccount } = await import("../account/AbstractKeylessAccount.js");
// Match `AbstractKeylessAccount.fetchJWK`'s `publicKey` type so the
// `FederatedKeyless` branch is not miscast to `KeylessPublicKey`.
type KP =
| import("../core/crypto/keyless.js").KeylessPublicKey
| import("../core/crypto/federatedKeyless.js").FederatedKeylessPublicKey;
type KS = import("../core/crypto/keyless.js").KeylessSignature;
await AbstractKeylessAccount.fetchJWK({
aptosConfig,
publicKey: signedTxn.authenticator.sender.public_key.publicKey as KP,
kid: (signedTxn.authenticator.sender.signature.signature as KS).getJwkKid(),
});
}
}
} catch {
// Swallow diagnostic errors so we always rethrow the original submission error.
}
throw e;
}
}
export type FeePayerOrFeePayerAuthenticatorOrNeither =
| { feePayer: Account; feePayerAuthenticator?: never }
| { feePayer?: never; feePayerAuthenticator: AccountAuthenticator }
| { feePayer?: never; feePayerAuthenticator?: never };
export async function signAndSubmitTransaction(
args: FeePayerOrFeePayerAuthenticatorOrNeither & {
aptosConfig: AptosConfig;
signer: Account;
transaction: AnyRawTransaction;
} & InputTransactionPluginData,
): Promise<PendingTransactionResponse> {
const { aptosConfig, signer, feePayer, transaction, ...rest } = args;
// If the signer contains a KeylessAccount, await proof fetching in case the proof
// was fetched asynchronously.
if (isKeylessSigner(signer)) {
await signer.checkKeylessAccountValidity(aptosConfig);
}
if (isKeylessSigner(feePayer)) {
await feePayer.checkKeylessAccountValidity(aptosConfig);
}
if (
transaction.rawTransaction.payload instanceof TransactionPayloadEncryptedPayload &&
(isKeylessSigner(signer) || isKeylessSigner(feePayer))
) {
throw new Error(
"Encrypted transactions are not supported with keyless or federated keyless signers (signature mutability breaks payload binding).",
);
}
const feePayerAuthenticator =
args.feePayerAuthenticator || (feePayer && signAsFeePayer({ signer: feePayer, transaction }));
const senderAuthenticator = signTransaction({ signer, transaction });
return submitTransaction({
aptosConfig,
transaction,
senderAuthenticator,
feePayerAuthenticator,
...rest,
});
}
export async function signAndSubmitAsFeePayer(
args: {
aptosConfig: AptosConfig;
feePayer: Account;
senderAuthenticator: AccountAuthenticator;
transaction: AnyRawTransaction;
} & InputTransactionPluginData,
): Promise<PendingTransactionResponse> {
const { aptosConfig, senderAuthenticator, feePayer, transaction, ...rest } = args;
if (isKeylessSigner(feePayer)) {
await feePayer.checkKeylessAccountValidity(aptosConfig);
}
const feePayerAuthenticator = signAsFeePayer({ signer: feePayer, transaction });
return submitTransaction({
aptosConfig,
transaction,
senderAuthenticator,
feePayerAuthenticator,
...rest,
});
}
// Lazy-initialized to avoid circular dependency issues at module evaluation time.
let _packagePublishAbi: EntryFunctionABI | undefined;
function getPackagePublishAbi(): EntryFunctionABI {
if (!_packagePublishAbi) {
_packagePublishAbi = {
typeParameters: [],
parameters: [TypeTagVector.u8(), new TypeTagVector(TypeTagVector.u8())],
};
}
return _packagePublishAbi;
}
/**
* Publishes a package transaction to the Aptos blockchain.
* This function allows you to create and send a transaction that publishes a package with the specified metadata and bytecode.
*
* @param args - The arguments for the package transaction.
* @param args.aptosConfig - The configuration settings for the Aptos client.
* @param args.account - The address of the account sending the transaction.
* @param args.metadataBytes - The metadata associated with the package, represented as hexadecimal input.
* @param args.moduleBytecode - An array of module bytecode, each represented as hexadecimal input.
* @param args.options - Optional parameters for generating the transaction.
* @group Implementation
*/
export async function publicPackageTransaction(args: {
aptosConfig: AptosConfig;
account: AccountAddressInput;
metadataBytes: HexInput;
moduleBytecode: Array<HexInput>;
options?: InputGenerateTransactionOptions;
}): Promise<SimpleTransaction> {
const { aptosConfig, account, metadataBytes, moduleBytecode, options } = args;
const totalByteCode = moduleBytecode.map((bytecode) => MoveVector.U8(bytecode));
return generateTransaction({
aptosConfig,
sender: AccountAddress.from(account),
data: {
function: "0x1::code::publish_package_txn",
functionArguments: [MoveVector.U8(metadataBytes), new MoveVector(totalByteCode)],
abi: getPackagePublishAbi(),
},
options,
});
}