Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtianz/new script composer #587

Merged
merged 14 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to the Aptos TypeScript SDK will be captured in this file. This changelog is written by hand for now. It adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Unreleased
- [`Breaking`] Revert new `scriptComposer` api in transactionSubmission api to allow SDK callers to invoke multiple Move functions inside a same transaction and compose the calls dynamically.
runtian-zhou marked this conversation as resolved.
Show resolved Hide resolved

# 1.33.2 (2025-01-22)

Expand All @@ -29,7 +30,6 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
# 1.32.1 (2024-11-11)

- Add support for Firebase issuers in the `updateFederatedKeylessJwkSetTransaction` function
- [`Breaking`] Revert new `scriptComposer` api in transactionSubmission api to allow SDK callers to invoke multiple Move functions inside a same transaction and compose the calls dynamically.

# 1.32.0 (2024-11-08)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@noble/hashes": "^1.4.0",
"@scure/bip32": "^1.4.0",
"@scure/bip39": "^1.3.0",
"@aptos-labs/script-composer-pack": "^0.0.4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to latest when possible

"eventemitter3": "^5.0.1",
"form-data": "^4.0.0",
"js-base64": "^3.7.7",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 85 additions & 2 deletions src/api/transactionSubmission/build.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { AccountAddressInput } from "../../core";
import { AccountAddress, AccountAddressInput } from "../../core";
import { generateTransaction } from "../../internal/transactionSubmission";
import { InputGenerateTransactionPayloadData, InputGenerateTransactionOptions } from "../../transactions";
import {
InputGenerateTransactionPayloadData,
InputGenerateTransactionOptions,
AptosScriptComposer,
TransactionPayloadScript,
generateRawTransaction,
} from "../../transactions";
import { MultiAgentTransaction } from "../../transactions/instances/multiAgentTransaction";
import { SimpleTransaction } from "../../transactions/instances/simpleTransaction";
import { AptosConfig } from "../aptosConfig";
import { Deserializer } from "../../bcs";

/**
* A class to handle all `Build` transaction operations.
Expand Down Expand Up @@ -96,6 +103,82 @@
return generateTransaction({ aptosConfig: this.config, ...args });
}

/**
* Build a transaction from a series of Move calls.
*
* This function allows you to create a transaction with a list of Move calls.
*
* Right now we only tested this logic with single signer and we will add support
* for mutli agent transactions if needed.
*
* @param args.sender - The sender account address.
* @param args.builder - The closure to construct the list of calls.
* @param args.options - Optional transaction configurations.
* @param args.withFeePayer - Whether there is a fee payer for the transaction.
*
* @returns SimpleTransaction
*
* @example
* ```typescript
* import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
*
* const config = new AptosConfig({ network: Network.TESTNET });
* const aptos = new Aptos(config);
*
* async function runExample() {
* // Build a transaction from a chained series of Move calls.
* const transaction = await aptos.transaction.build.scriptComposer({
* sender: "0x1", // replace with a real sender account address
* builder: builder: async (builder) => {
* const coin = await builder.addBatchedCalls({
* function: "0x1::coin::withdraw",
* functionArguments: [CallArgument.new_signer(0), 1],
* typeArguments: ["0x1::aptos_coin::AptosCoin"],
* });
*
* // Pass the returned value from the first function call to the second call
* const fungibleAsset = await builder.addBatchedCalls({
* function: "0x1::coin::coin_to_fungible_asset",
* functionArguments: [coin[0]],
* typeArguments: ["0x1::aptos_coin::AptosCoin"],
* });
*
* await builder.addBatchedCalls({
* function: "0x1::primary_fungible_store::deposit",
* functionArguments: [singleSignerED25519SenderAccount.accountAddress, fungibleAsset[0]],
* typeArguments: [],
* });
* return builder;
* },
* options: {
* gasUnitPrice: 100, // specify your own gas unit price if needed
* maxGasAmount: 1000, // specify your own max gas amount if needed
* },
* });
*
* console.log(transaction);
* }
* runExample().catch(console.error);
* ```
*/
async scriptComposer(args: {
sender: AccountAddressInput;
builder: (builder: AptosScriptComposer) => Promise<AptosScriptComposer>;
options?: InputGenerateTransactionOptions;
withFeePayer?: boolean;
}): Promise<SimpleTransaction> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we are not following the other transactions build flow format?

return generateTransaction({ aptosConfig: this.config, ...args });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the generateTransaction cannot take the serialized blob of move script as input (input contains both bytecode and type arguments/inputs).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would have generateBatchTransaction

let composer = new AptosScriptComposer(this.config);

Check failure on line 170 in src/api/transactionSubmission/build.ts

View workflow job for this annotation

GitHub Actions / run-tests

'composer' is never reassigned. Use 'const' instead
await composer.init();
const builder = await args.builder(composer);
const bytes = builder.build();
const rawTxn = await generateRawTransaction({
aptosConfig: this.config,
payload: TransactionPayloadScript.load(new Deserializer(bytes)),
...args,
});
return new SimpleTransaction(rawTxn, args.withFeePayer === true ? AccountAddress.ZERO : undefined);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

/**
* Build a multi-agent transaction that allows multiple signers to authorize a transaction.
*
Expand Down
1 change: 1 addition & 0 deletions src/transactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./transactionBuilder";
export * from "./typeTag";
export * from "./typeTag/parser";
export * from "./types";
export * from "./script-composer";
85 changes: 85 additions & 0 deletions src/transactions/script-composer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright © Aptos Foundation
Copy link
Contributor

@GhostWalker562 GhostWalker562 Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename directory from script-composer to scriptComposer

// SPDX-License-Identifier: Apache-2.0

import { AptosApiType } from "../../utils";
import { AptosConfig } from "../../api/aptosConfig";
import { InputBatchedFunctionData } from "../types";
import { fetchMoveFunctionAbi, getFunctionParts, standardizeTypeTags } from "../transactionBuilder";
import { CallArgument } from "../../types";
import { convertCallArgument } from "../transactionBuilder/remoteAbi";

// A wrapper class around TransactionComposer, which is a WASM library compiled
// from aptos-core/aptos-move/script-composer.
//
// This class allows the SDK caller to build a transaction that invokes multiple Move functions
// and allow for arguments to be passed around.
export class AptosScriptComposer {
private config: AptosConfig;

private builder?: any;

private static transactionComposer?: any;

constructor(aptosConfig: AptosConfig) {
this.config = aptosConfig;
this.builder = undefined;
}

// Initializing the wasm needed for the script composer, must be called
// before using the composer.
async init() {
if (!AptosScriptComposer.transactionComposer) {
const module = await import("@aptos-labs/script-composer-pack");
const { TransactionComposer, initSync, wasm } = module;
initSync({module: wasm});
AptosScriptComposer.transactionComposer = TransactionComposer;
}
this.builder = AptosScriptComposer.transactionComposer.single_signer();
}

// Add a move function invocation to the TransactionComposer.
//
// Similar to how to create an entry function, the difference is that input arguments could
// either be a `CallArgument` which represents an abstract value returned from a previous Move call
// or the regular entry function arguments.
//
// The function would also return a list of `CallArgument` that can be passed on to future calls.
async addBatchedCalls(input: InputBatchedFunctionData): Promise<CallArgument[]> {
const { moduleAddress, moduleName, functionName } = getFunctionParts(input.function);
const nodeUrl = this.config.getRequestUrl(AptosApiType.FULLNODE);

// Load the calling module into the builder.
await this.builder.load_module(nodeUrl, `${moduleAddress}::${moduleName}`);

// Load the calling type arguments into the loader.
if (input.typeArguments !== undefined) {
for (const typeTag of input.typeArguments) {
// eslint-disable-next-line no-await-in-loop
await this.builder.load_type_tag(nodeUrl, typeTag.toString());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to await Promise.all(input.typeArguments.map((typeTag) => this.builder.load_type_tag(nodeUrl, typeTag.toString()))); to await the promises in parellel

}
const typeArguments = standardizeTypeTags(input.typeArguments);
const functionAbi = await fetchMoveFunctionAbi(moduleAddress, moduleName, functionName, this.config);
// 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}`,
);
}

const functionArguments: CallArgument[] = input.functionArguments.map((arg, i) =>
convertCallArgument(arg, functionName, functionAbi, i, typeArguments),
);

return this.builder.add_batched_call(
`${moduleAddress}::${moduleName}`,
functionName,
typeArguments.map((arg) => arg.toString()),
functionArguments,
);
}

build(): Uint8Array {
return this.builder.generate_batched_calls(true);
}
}
56 changes: 55 additions & 1 deletion src/transactions/transactionBuilder/remoteAbi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
throwTypeMismatch,
convertNumber,
} from "./helpers";
import { MoveFunction } from "../../types";
import { CallArgument, MoveFunction } from "../../types";

const TEXT_ENCODER = new TextEncoder();

Expand Down Expand Up @@ -96,6 +96,34 @@ export async function fetchFunctionAbi(
return undefined;
}

/**
* Fetches a function ABI from the on-chain module ABI. It doesn't validate whether it's a view or entry function.
* @param moduleAddress
* @param moduleName
* @param functionName
* @param aptosConfig
*/
export async function fetchMoveFunctionAbi(
moduleAddress: string,
moduleName: string,
functionName: string,
aptosConfig: AptosConfig,
): Promise<FunctionABI> {
const functionAbi = await fetchFunctionAbi(moduleAddress, moduleName, functionName, aptosConfig);
if (!functionAbi) {
throw new Error(`Could not find function ABI for '${moduleAddress}::${moduleName}::${functionName}'`);
}
const params: TypeTag[] = [];
for (let i = 0; i < functionAbi.params.length; i += 1) {
params.push(parseTypeTag(functionAbi.params[i], { allowGenerics: true }));
}

return {
typeParameters: functionAbi.generic_type_params,
parameters: params,
};
}

/**
* Fetches the ABI for an entry function from the specified module address.
* This function validates if the ABI corresponds to an entry function and retrieves its parameters.
Expand Down Expand Up @@ -191,6 +219,32 @@ export async function fetchViewFunctionAbi(
};
}

/**
* Converts a entry function argument into CallArgument, if necessary.
* This function checks the provided argument against the expected parameter type and converts it accordingly.
*
* @param functionName - The name of the function for which the argument is being converted.
* @param functionAbi - The ABI (Application Binary Interface) of the function, which defines its parameters.
* @param argument - The argument to be converted, which can be of various types. If the argument is already
* CallArgument returned from TransactionComposer it would be returned immediately.
* @param position - The index of the argument in the function's parameter list.
* @param genericTypeParams - An array of type tags for any generic type parameters.
*/
export function convertCallArgument(
argument: CallArgument | EntryFunctionArgumentTypes | SimpleEntryFunctionArgumentTypes,
functionName: string,
functionAbi: FunctionABI,
position: number,
genericTypeParams: Array<TypeTag>,
): CallArgument {
if (argument instanceof CallArgument) {
return argument;
}
return CallArgument.new_bytes(
convertArgument(functionName, functionAbi, argument, position, genericTypeParams).bcsToBytes(),
);
}

/**
* Converts a non-BCS encoded argument into BCS encoded, if necessary.
* This function checks the provided argument against the expected parameter type and converts it accordingly.
Expand Down
11 changes: 11 additions & 0 deletions src/transactions/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { CallArgument } from "@aptos-labs/script-composer-pack";
import { AptosConfig } from "../api/aptosConfig";
import { MoveOption, MoveString, MoveVector } from "../bcs/serializable/moveStructs";
import { Bool, U128, U16, U256, U32, U64, U8 } from "../bcs/serializable/movePrimitives";
Expand Down Expand Up @@ -192,6 +193,16 @@ export type InputMultiSigDataWithABI = {
* @category Transactions
*/
export type InputEntryFunctionDataWithRemoteABI = InputEntryFunctionData & { aptosConfig: AptosConfig };

/**
* The data needed to generate a batched function payload
*/
export type InputBatchedFunctionData = {
function: MoveFunctionId;
typeArguments?: Array<TypeArgument>;
functionArguments: Array<EntryFunctionArgumentTypes | CallArgument | SimpleEntryFunctionArgumentTypes>;
};

/**
* The data needed to generate a Multi Sig payload
* @group Implementation
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./indexer";
export * from "./types";
export { CallArgument } from "@aptos-labs/script-composer-pack";
Loading
Loading