Skip to content

Commit

Permalink
Implement a new api for building script payload (#565)
Browse files Browse the repository at this point in the history
* import aptos-intent

* Fix some lints

* Fix comments

* Fix deps, re-export CallArgument

* fixup! Fix deps, re-export CallArgument

* Fix comments

* fix lint

* fixup! fix lint

* Update package
  • Loading branch information
runtian-zhou authored Nov 7, 2024
1 parent f644e61 commit a301268
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 2 deletions.
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 {
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);
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

0 comments on commit a301268

Please sign in to comment.