UNPKG

@aptos-labs/ts-sdk

Version:
579 lines (522 loc) 22 kB
// Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 /** * This file handles the transaction creation lifecycle. * It holds different operations to generate a transaction payload, a raw transaction, * and a signed transaction that can be simulated, signed and submitted to chain. */ import { sha3_256 as sha3Hash } from "@noble/hashes/sha3"; import { AptosConfig } from "../../api/aptosConfig"; import { AccountAddress, AccountAddressInput, Hex, PublicKey } from "../../core"; import { AnyPublicKey, AnySignature, Secp256k1PublicKey, Secp256k1Signature } from "../../core/crypto"; import { Ed25519PublicKey, Ed25519Signature } from "../../core/crypto/ed25519"; import { getInfo } from "../../internal/account"; import { getLedgerInfo } from "../../internal/general"; import { getGasPriceEstimation } from "../../internal/transaction"; import { NetworkToChainId } from "../../utils/apiEndpoints"; import { DEFAULT_MAX_GAS_AMOUNT, DEFAULT_TXN_EXP_SEC_FROM_NOW } from "../../utils/const"; import { normalizeBundle } from "../../utils/normalizeBundle"; import { AccountAuthenticator, AccountAuthenticatorEd25519, AccountAuthenticatorSingleKey, } from "../authenticator/account"; import { TransactionAuthenticator, TransactionAuthenticatorEd25519, TransactionAuthenticatorFeePayer, TransactionAuthenticatorMultiAgent, TransactionAuthenticatorSingleSender, } from "../authenticator/transaction"; import { ChainId, EntryFunction, FeePayerRawTransaction, MultiAgentRawTransaction, MultiSig, MultiSigTransactionPayload, RawTransaction, Script, TransactionPayloadEntryFunction, TransactionPayloadMultiSig, TransactionPayloadScript, } from "../instances"; import { SignedTransaction } from "../instances/signedTransaction"; import { AnyRawTransaction, AnyTransactionPayloadInstance, EntryFunctionArgumentTypes, InputGenerateMultiAgentRawTransactionArgs, InputGenerateRawTransactionArgs, InputGenerateSingleSignerRawTransactionArgs, InputGenerateTransactionOptions, InputScriptData, InputSimulateTransactionData, InputMultiSigDataWithRemoteABI, InputEntryFunctionDataWithRemoteABI, InputGenerateTransactionPayloadDataWithRemoteABI, InputSubmitTransactionData, InputGenerateTransactionPayloadDataWithABI, InputEntryFunctionDataWithABI, InputMultiSigDataWithABI, InputViewFunctionDataWithRemoteABI, InputViewFunctionDataWithABI, FunctionABI, } from "../types"; import { convertArgument, fetchEntryFunctionAbi, fetchViewFunctionAbi, standardizeTypeTags } from "./remoteAbi"; import { memoizeAsync } from "../../utils/memoize"; import { getFunctionParts, isScriptDataInput } from "./helpers"; import { SimpleTransaction } from "../instances/simpleTransaction"; import { MultiAgentTransaction } from "../instances/multiAgentTransaction"; /** * We are defining function signatures, each with its specific input and output. * These are the possible function signature for our `generateTransactionPayload` function. * When we call our `generateTransactionPayload` function with the relevant type properties, * Typescript can infer the return type based on the appropriate function overload. */ export async function generateTransactionPayload(args: InputScriptData): Promise<TransactionPayloadScript>; export async function generateTransactionPayload( args: InputEntryFunctionDataWithRemoteABI, ): Promise<TransactionPayloadEntryFunction>; export async function generateTransactionPayload( args: InputMultiSigDataWithRemoteABI, ): Promise<TransactionPayloadMultiSig>; /** * Builds a transaction payload based on the data argument and returns * a transaction payload - TransactionPayloadScript | TransactionPayloadMultiSig | TransactionPayloadEntryFunction * * This uses the RemoteABI by default, and the remote ABI can be skipped by using generateTransactionPayloadWithABI * * @param args.data GenerateTransactionPayloadData * * @return TransactionPayload */ export async function generateTransactionPayload( args: InputGenerateTransactionPayloadDataWithRemoteABI, ): Promise<AnyTransactionPayloadInstance> { if (isScriptDataInput(args)) { return generateTransactionPayloadScript(args); } 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 }); } export function generateTransactionPayloadWithABI(args: InputEntryFunctionDataWithABI): TransactionPayloadEntryFunction; export function generateTransactionPayloadWithABI(args: InputMultiSigDataWithABI): TransactionPayloadMultiSig; export function generateTransactionPayloadWithABI( args: InputGenerateTransactionPayloadDataWithABI, ): AnyTransactionPayloadInstance { const functionAbi = args.abi; const { moduleAddress, moduleName, functionName } = getFunctionParts(args.function); // Ensure that all type arguments are typed properly const typeArguments = standardizeTypeTags(args.typeArguments); // Check the type argument count against the ABI if (typeArguments.length !== functionAbi.typeParameters.length) { throw new Error( `Type argument count mismatch, expected ${functionAbi.typeParameters.length}, received ${typeArguments.length}`, ); } // Check all BCS types, and convert any non-BCS types const functionArguments: Array<EntryFunctionArgumentTypes> = args.functionArguments.map((arg, i) => convertArgument(args.function, functionAbi, arg, i, typeArguments), ); // Check that all arguments are accounted for if (functionArguments.length !== functionAbi.parameters.length) { throw new Error( // eslint-disable-next-line max-len `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${functionAbi.parameters.length} but got ${functionArguments.length}`, ); } // Generate entry function payload const entryFunctionPayload = EntryFunction.build( `${moduleAddress}::${moduleName}`, functionName, typeArguments, 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)), ); } // Otherwise send as an entry function return new TransactionPayloadEntryFunction(entryFunctionPayload); } export async function generateViewFunctionPayload(args: InputViewFunctionDataWithRemoteABI): Promise<EntryFunction> { const { moduleAddress, moduleName, functionName } = getFunctionParts(args.function); const functionAbi = await fetchAbi({ key: "view-function", moduleAddress, moduleName, functionName, aptosConfig: args.aptosConfig, abi: args.abi, fetch: fetchViewFunctionAbi, }); // Fill in the ABI return generateViewFunctionPayloadWithABI({ abi: functionAbi, ...args }); } export function generateViewFunctionPayloadWithABI(args: InputViewFunctionDataWithABI): EntryFunction { const functionAbi = args.abi; const { moduleAddress, moduleName, functionName } = getFunctionParts(args.function); // Ensure that all type arguments are typed properly const typeArguments = standardizeTypeTags(args.typeArguments); // Check the type argument count against the ABI if (typeArguments.length !== functionAbi.typeParameters.length) { throw new Error( `Type argument count mismatch, expected ${functionAbi.typeParameters.length}, received ${typeArguments.length}`, ); } // Check all BCS types, and convert any non-BCS types const functionArguments: Array<EntryFunctionArgumentTypes> = args?.functionArguments?.map((arg, i) => convertArgument(args.function, functionAbi, arg, i, typeArguments)) ?? []; // Check that all arguments are accounted for if (functionArguments.length !== functionAbi.parameters.length) { throw new Error( // eslint-disable-next-line max-len `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${functionAbi.parameters.length} but got ${functionArguments.length}`, ); } // Generate entry function payload return EntryFunction.build(`${moduleAddress}::${moduleName}`, functionName, typeArguments, functionArguments); } function generateTransactionPayloadScript(args: InputScriptData) { return new TransactionPayloadScript( new Script(Hex.fromHexInput(args.bytecode).toUint8Array(), args.typeArguments ?? [], args.functionArguments), ); } /** * Generates a raw transaction * * @param args.aptosConfig AptosConfig * @param args.sender The transaction's sender account address as a hex input * @param args.payload The transaction payload - can create by using generateTransactionPayload() * * @returns RawTransaction */ export async function generateRawTransaction(args: { aptosConfig: AptosConfig; sender: AccountAddressInput; payload: AnyTransactionPayloadInstance; options?: InputGenerateTransactionOptions; feePayerAddress?: AccountAddressInput; }): Promise<RawTransaction> { const { aptosConfig, sender, payload, options, feePayerAddress } = args; const getChainId = async () => { if (NetworkToChainId[aptosConfig.network]) { return { chainId: NetworkToChainId[aptosConfig.network] }; } const info = await getLedgerInfo({ aptosConfig }); return { chainId: info.chain_id }; }; const getGasUnitPrice = async () => { if (options?.gasUnitPrice) { return { gasEstimate: options.gasUnitPrice }; } const estimation = await getGasPriceEstimation({ aptosConfig }); return { gasEstimate: estimation.gas_estimate }; }; const getSequenceNumberForAny = async () => { const getSequenceNumber = async () => { if (options?.accountSequenceNumber !== undefined) { return options.accountSequenceNumber; } return (await getInfo({ aptosConfig, accountAddress: sender })).sequence_number; }; /** * Check if is sponsored transaction to honor AIP-52 * {@link https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-52.md} */ if (feePayerAddress && AccountAddress.from(feePayerAddress).equals(AccountAddress.ZERO)) { // Handle sponsored transaction generation with the option that // the main signer has not been created on chain try { // Check if main signer has been created on chain, if not assign sequence number 0 return await getSequenceNumber(); } catch (e: any) { return 0; } } else { return getSequenceNumber(); } }; const [{ chainId }, { gasEstimate }, sequenceNumber] = await Promise.all([ getChainId(), getGasUnitPrice(), getSequenceNumberForAny(), ]); const { maxGasAmount, gasUnitPrice, expireTimestamp } = { maxGasAmount: options?.maxGasAmount ? BigInt(options.maxGasAmount) : BigInt(DEFAULT_MAX_GAS_AMOUNT), gasUnitPrice: options?.gasUnitPrice ?? BigInt(gasEstimate), expireTimestamp: options?.expireTimestamp ?? BigInt(Math.floor(Date.now() / 1000) + DEFAULT_TXN_EXP_SEC_FROM_NOW), }; return new RawTransaction( AccountAddress.from(sender), BigInt(sequenceNumber), payload, BigInt(maxGasAmount), BigInt(gasUnitPrice), BigInt(expireTimestamp), new ChainId(chainId), ); } /** * We are defining function signatures, each with its specific input and output. * These are the possible function signature for our `generateTransaction` function. * When we call our `generateTransaction` function with the relevant type properties, * Typescript can infer the return type based on the appropriate function overload. */ export async function buildTransaction(args: InputGenerateSingleSignerRawTransactionArgs): Promise<SimpleTransaction>; export async function buildTransaction(args: InputGenerateMultiAgentRawTransactionArgs): Promise<MultiAgentTransaction>; /** * Generates a transaction based on the provided arguments * * Note: we can start with one function to support all different payload/transaction types, * and if to complex to use, we could have function for each type * * @param args.aptosConfig AptosConfig * @param args.sender The transaction's sender account address as a hex input * @param args.payload The transaction payload - can create by using generateTransactionPayload() * @param args.options optional. Transaction options object * @param args.secondarySignerAddresses optional. For when want to create a multi signers transaction * @param args.feePayerAddress optional. For when want to create a fee payer (aka sponsored) transaction * * @return An instance of a RawTransaction, plus optional secondary/fee payer addresses * ``` * { * rawTransaction: RawTransaction, * secondarySignerAddresses? : Array<AccountAddress>, * feePayerAddress?: AccountAddress * } * ``` */ export async function buildTransaction(args: InputGenerateRawTransactionArgs): Promise<AnyRawTransaction> { const { aptosConfig, sender, payload, options, feePayerAddress } = args; // generate raw transaction const rawTxn = await generateRawTransaction({ aptosConfig, sender, payload, options, feePayerAddress, }); // if multi agent transaction if ("secondarySignerAddresses" in args) { const signers: Array<AccountAddress> = args.secondarySignerAddresses?.map((signer) => AccountAddress.from(signer)) ?? []; return new MultiAgentTransaction( rawTxn, signers, args.feePayerAddress ? AccountAddress.from(args.feePayerAddress) : undefined, ); } // return the raw transaction return new SimpleTransaction(rawTxn, args.feePayerAddress ? AccountAddress.from(args.feePayerAddress) : undefined); } /** * Simulate a transaction before signing and submit to chain * * @param args.transaction A aptos transaction type to sign * @param args.signerPublicKey The signer public key * @param args.secondarySignersPublicKeys optional. The secondary signers public keys if multi signers transaction * @param args.feePayerPublicKey optional. The fee payer public key is a fee payer (aka sponsored) transaction * @param args.options optional. SimulateTransactionOptions * * @returns A signed serialized transaction that can be simulated */ export function generateSignedTransactionForSimulation(args: InputSimulateTransactionData): Uint8Array { const { signerPublicKey, transaction, secondarySignersPublicKeys, feePayerPublicKey } = args; const accountAuthenticator = getAuthenticatorForSimulation(signerPublicKey); // fee payer transaction if (transaction.feePayerAddress) { const transactionToSign = new FeePayerRawTransaction( transaction.rawTransaction, transaction.secondarySignerAddresses ?? [], transaction.feePayerAddress, ); let secondaryAccountAuthenticators: Array<AccountAuthenticator> = []; if (secondarySignersPublicKeys) { secondaryAccountAuthenticators = secondarySignersPublicKeys.map((publicKey) => getAuthenticatorForSimulation(publicKey), ); } const feePayerAuthenticator = getAuthenticatorForSimulation(feePayerPublicKey!); const transactionAuthenticator = new TransactionAuthenticatorFeePayer( accountAuthenticator, transaction.secondarySignerAddresses ?? [], secondaryAccountAuthenticators, { address: transaction.feePayerAddress, authenticator: feePayerAuthenticator, }, ); return new SignedTransaction(transactionToSign.raw_txn, transactionAuthenticator).bcsToBytes(); } // multi agent transaction if (transaction.secondarySignerAddresses) { const transactionToSign = new MultiAgentRawTransaction( transaction.rawTransaction, transaction.secondarySignerAddresses, ); let secondaryAccountAuthenticators: Array<AccountAuthenticator> = []; secondaryAccountAuthenticators = secondarySignersPublicKeys!.map((publicKey) => getAuthenticatorForSimulation(publicKey), ); const transactionAuthenticator = new TransactionAuthenticatorMultiAgent( accountAuthenticator, transaction.secondarySignerAddresses, secondaryAccountAuthenticators, ); return new SignedTransaction(transactionToSign.raw_txn, transactionAuthenticator).bcsToBytes(); } // single signer raw transaction let transactionAuthenticator; if (accountAuthenticator instanceof AccountAuthenticatorEd25519) { transactionAuthenticator = new TransactionAuthenticatorEd25519( accountAuthenticator.public_key, accountAuthenticator.signature, ); } else if (accountAuthenticator instanceof AccountAuthenticatorSingleKey) { transactionAuthenticator = new TransactionAuthenticatorSingleSender(accountAuthenticator); } else { throw new Error("Invalid public key"); } return new SignedTransaction(transaction.rawTransaction, transactionAuthenticator).bcsToBytes(); } export function getAuthenticatorForSimulation(publicKey: PublicKey) { // TODO add support for AnyMultiKey if (publicKey instanceof AnyPublicKey) { if (publicKey.publicKey instanceof Ed25519PublicKey) { return new AccountAuthenticatorSingleKey(publicKey, new AnySignature(new Ed25519Signature(new Uint8Array(64)))); } if (publicKey.publicKey instanceof Secp256k1PublicKey) { return new AccountAuthenticatorSingleKey(publicKey, new AnySignature(new Secp256k1Signature(new Uint8Array(64)))); } } // legacy code return new AccountAuthenticatorEd25519( new Ed25519PublicKey(publicKey.toUint8Array()), new Ed25519Signature(new Uint8Array(64)), ); } /** * Prepare a transaction to be submitted to chain * * @param args.transaction A aptos transaction type * @param args.senderAuthenticator The account authenticator of the transaction sender * @param args.secondarySignerAuthenticators optional. For when the transaction is a multi signers transaction * * @returns A SignedTransaction */ export function generateSignedTransaction(args: InputSubmitTransactionData): Uint8Array { const { transaction, feePayerAuthenticator, additionalSignersAuthenticators } = args; const senderAuthenticator = normalizeBundle(AccountAuthenticator, args.senderAuthenticator); let txnAuthenticator: TransactionAuthenticator; if (transaction.feePayerAddress) { if (!feePayerAuthenticator) { throw new Error("Must provide a feePayerAuthenticator argument to generate a signed fee payer transaction"); } txnAuthenticator = new TransactionAuthenticatorFeePayer( senderAuthenticator, transaction.secondarySignerAddresses ?? [], additionalSignersAuthenticators ?? [], { address: transaction.feePayerAddress, authenticator: feePayerAuthenticator, }, ); } else if (transaction.secondarySignerAddresses) { if (!additionalSignersAuthenticators) { throw new Error( "Must provide a additionalSignersAuthenticators argument to generate a signed multi agent transaction", ); } txnAuthenticator = new TransactionAuthenticatorMultiAgent( senderAuthenticator, transaction.secondarySignerAddresses, additionalSignersAuthenticators, ); } else if (senderAuthenticator instanceof AccountAuthenticatorEd25519) { txnAuthenticator = new TransactionAuthenticatorEd25519( senderAuthenticator.public_key, senderAuthenticator.signature, ); } else { txnAuthenticator = new TransactionAuthenticatorSingleSender(senderAuthenticator); } return new SignedTransaction(transaction.rawTransaction, txnAuthenticator).bcsToBytes(); } /** * Hashes the set of values with a SHA-3 256 hash * @param input array of UTF-8 strings or Uint8array byte arrays */ export function hashValues(input: (Uint8Array | string)[]): Uint8Array { const hash = sha3Hash.create(); for (const item of input) { hash.update(item); } return hash.digest(); } /** * The domain separated prefix for hashing transacitons */ const TRANSACTION_PREFIX = hashValues(["APTOS::Transaction"]); /** * Generates a user transaction hash for the given transaction payload. It must already have an authenticator * @param args InputSubmitTransactionData */ export function generateUserTransactionHash(args: InputSubmitTransactionData): string { const signedTransaction = generateSignedTransaction(args); // Transaction signature is defined as, the domain separated prefix based on struct (Transaction) // Then followed by the type of the transaction for the enum, UserTransaction is 0 // Then followed by BCS encoded bytes of the signed transaction return new Hex(hashValues([TRANSACTION_PREFIX, new Uint8Array([0]), signedTransaction])).toString(); } /** * Fetches and caches ABIs with allowing for pass-through on provided ABIs * @param key * @param moduleAddress * @param moduleName * @param functionName * @param aptosConfig * @param abi * @param fetch */ async function fetchAbi<T extends FunctionABI>({ key, moduleAddress, moduleName, functionName, aptosConfig, abi, fetch, }: { key: string; moduleAddress: string; moduleName: string; functionName: string; aptosConfig: AptosConfig; abi?: T; fetch: (moduleAddress: string, moduleName: string, functionName: string, aptosConfig: AptosConfig) => Promise<T>; }): Promise<T> { if (abi !== undefined) { return abi; } // We fetch the entry function ABI, and then pretend that we already had the ABI return memoizeAsync( async () => fetch(moduleAddress, moduleName, functionName, aptosConfig), `${key}-${aptosConfig.network}-${moduleAddress}-${moduleName}-${functionName}`, 1000 * 60 * 5, // 5 minutes )(); }