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

Implement a new api for building script payload #565

Merged
merged 9 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
- Includes the address in the `AbstractKeylessAccount` serialization to prevent information loss for key rotated accounts.
- [`Breaking`] Deprecate `serializeOptionStr` and `deserializeOptionStr` in favor of `serializeOption` and `deserializeOption`.
- [`Breaking`] Renames `KeylessConfiguration.verficationKey` to `verificationKey`
- Add a new `scriptComposer` api in transactionSubmission api to allower SDK callers to invoke multiple Move functions inside a same transaction and compose the calls dynamically.

# 1.31.0 (2024-10-24)

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",
"@wgb5445/aptos-intent-npm": "^0.1.9",
"eventemitter3": "^5.0.1",
"form-data": "^4.0.0",
"js-base64": "^3.7.7",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

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

83 changes: 82 additions & 1 deletion src/api/transactionSubmission/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@

import { 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 @@ -93,6 +100,80 @@ export class Build {
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.script_composer({
* 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> {
const builder = await args.builder(new AptosScriptComposer(this.config));
const bytes = builder.build();
const rawTxn = await generateRawTransaction({
aptosConfig: this.config,
payload: TransactionPayloadScript.load(new Deserializer(bytes)),
...args,
});
return new SimpleTransaction(rawTxn);
}

/**
* 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";
76 changes: 76 additions & 0 deletions src/transactions/script-composer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { TransactionComposer, initSync, create_wasm } from "@wgb5445/aptos-intent-npm";
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";

(async () => {
initSync(await create_wasm());
})();

// 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 {
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
private builder: TransactionComposer;

private config: AptosConfig;

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

// 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());
}
}
const typeArguments = standardizeTypeTags(input.typeArguments);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to keep the format same as the other transaction generation logic in the SDK and move the whole function generator part to the transactionBuilder.ts file and have it use the memoize logic - same as we do for any other payload https://github.com/aptos-labs/aptos-ts-sdk/blob/main/src/transactions/transactionBuilder/transactionBuilder.ts#L124

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 @@ -92,6 +92,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 @@ -183,6 +211,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 "@wgb5445/aptos-intent-npm";
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 @@ -164,6 +165,16 @@ export type InputMultiSigDataWithABI = {
* Combines input function data with Aptos configuration for remote ABI interactions.
*/
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
*/
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 "@wgb5445/aptos-intent-npm";
Loading
Loading