Skip to content

Dynamic Script Composer

Develop a new set of APIs in our SDKs to allow application builders to easily chain multiple Move operations in a single transaction.

In this AIP, we will focus solely on the transaction-building aspect. While this solution aims to enhance the composability of our ecosystem as part of the Aptos Intent standard, those standards will be proposed in future AIPs. This AIP will serve as a reference for those later efforts.

Currently, there are two ways to interact with the Aptos Blockchain:

  • EntryFunction Payload: Developers need only specify the function name they want to invoke and pass the corresponding arguments to the SDK.
  • Script Payload: Developers require a Move compiler to compile Move scripts into a compiled format, which is then used as the transaction payload.

These two approaches offer significantly different developer experiences. EntryFunctionPayload is easy to build using our provided SDK, but requires the developer to deploy the entry function to the blockchain upfront, limiting composability. Script Payload provides full expressivity, allowing developers to call and compose any on-chain Move functions. However, it requires the use of the full Move compiler, which may not be practical in a browser-based environment. Currently, the only practical way to use a script is to compile it once, store the compiled bytes in binary format, and use that format in the SDK on the client side.

Ideally, we want a solution that strikes a balance between these two approaches: allowing developers to easily compose Move functions in transactions on the client side without needing the entire Move compiler.

We present the Dynamic Transaction Composer to address this challenge. The transaction builder API will be integrated into the SDK, allowing developers to chain multiple Move calls into a single transaction. The builder will be implemented in Rust, which will generate compiled Move script bytes directly from a sequence of Move calls, without requiring the full Move compiler or source code. This builder will be bundled into WASM for seamless integration into our TypeScript SDK.

Additionally, we will introduce annotation logic to the indexer API so that the indexer/explorer can display the script in a format that looks like the chained function calls. This can be achieved by implementing a decompiler that reverse-engineers the code generation logic.

Given the difficulty of deploying Move script compiler onto user’s client side, dapp developers usually have no choice but to encode all the ways in which a user may interact with their application in an entry function that is published on chain. This resulted in smart contracts being less composable and didn’t fully leverage the composability of Move.

For example, despite we have notion for FungibleAsset in our framework, the DeFi application cannot take FungibleAsset as input to their entry function. Instead, they will need to invoke primary_fungible_store::withdraw to get the FungibleAsset they need. Similarly in that entry function, they would need to deposit the FungibleAsset in order to pass Move’s bytecode verifier. As a result, it is very hard to chain the calls together to perform multi-hop swaps using the DeFi’s existing entry functions.

Another problem issue that would be mitigated by this AIP is that an entry function is opaque and there is no way to be explicit from the user about the intent of their transaction. There’s no way for a user to tell how much FungibleAsset would be withdrawed from this transaction. We are working on a permissioned signer solution that would mitigate this issue but we could completely get around this issue by askign smart contracts to expose interfaces that could take assets directly, and use the builder to chain up the calls.

With this solution, we can construct a DeFi ecosystem that is both safer and more composable by chaining up following move calls:

  1. User withdraw FungibleAsset from their FungibleStore.
  2. User pass the withdrawed FungibleAsset to the DeFi contract. This is safer as the user will no long need to pass &signer into the DeFi contract that would be used to withdraw the FungibleAsset.
  3. (Let’s say the user calls an AMM protocol) AMM should return the user the FungibleAsset that the user desires.
  4. User can chain the returned value from step (3) with a call into another AMM protocol until the user get the expected asset.
  5. User deposit the asset into its own FungibleStore.

An alternative approach is to introduce a third payload type in our system and have the node software interpret this payload directly. While this could offer some performance improvements by bypassing the full bytecode verifier, it would require reimplementing several critical checks, such as type safety, ability constraints, and reference safety, that are already handled by the verifier. Duplicating this logic at the node level not only adds redundancy but also increases the risk of introducing security vulnerabilities.

We will be introducing the following APIs in the ts-sdk:

export type InputBatchedFunctionData = {
function: MoveFunctionId;
typeArguments?: Array<TypeArgument>;
/* Function argument could either be existing entry function argument or come from the output of a previous function */
functionArguments: Array<EntryFunctionArgumentTypes | BatchArgument | SimpleEntryFunctionArgumentTypes>;
};
export class AptosScriptBuilder {
/* Move function calls can return values. Those values can be passed to other functions by move, copy or reference */
async add_move_call(input: InputBatchedFunctionData): Promise<BatchArgument>;
}
/* Existing TransactionBuilder */
export class Build {
async script_builder(args: {
sender: AccountAddressInput;
builder: (builder: AptosScriptBuilder) => Promise<AptosScriptBuilder>;
options?: InputGenerateTransactionOptions;
withFeePayer?: boolean;
}): Promise<SimpleTransaction>
}

With those new apis, developers can pass in a closure that builds up the Move call chains into one transaction. Here’s an example:

const transaction = await _aptos.aptos.transaction.build.script_builder({
sender: singleSignerED25519SenderAccount.accountAddress,
builder: async (builder) => {
/* return_1 should contain the withdrawed Coin<AptosCoin> */
let return_1 = await builder.add_move_call({
function: `0x1::coin::withdraw`,
functionArguments: [BatchArgument.new_signer(0), 1],
typeArguments: ["0x1::aptos_coin::AptosCoin"]
});
/* return_2 should contain the converted FungibleAsset */
let return_2 = await builder.add_move_call({
function: `0x1::coin::coin_to_fungible_asset`,
/* Pass the withdrawed Coin<AptosCoin> to the coin_to_fungible_asset */
functionArguments: [return_1],
typeArguments: ["0x1::aptos_coin::AptosCoin"]
});
/* Deposit fungible asset into
await builder.add_move_call({
function: `0x1::primary_fungible_store::deposit`,
functionArguments: [singleSignerED25519SenderAccount.accountAddress, return_2],
typeArguments: []
});
return builder;
}
});

The ts-sdk will be a WASM binary wrapper around a rust library that takes function call payloads and convert it into a serialized Move script that could be published to the blockchain. The WASM binary size should be minimal to be ported with the ts-sdk.

The rust library is implemented in: https://github.com/aptos-labs/aptos-core/tree/runtianz/aptos_intent

The ts-sdk change is implemented in: https://github.com/aptos-labs/aptos-ts-sdk/tree/runtianz/intent_sdk

We will extend this library to other sdks if we got explicit asks from the community.

Implemented tests in both rust library and ts-sdk.

Most of the changes should be on the client side so no significant risk to me.

See Risk.

We will be proposing Aptos Intent, which allowed users to specify conditional transfer of on-chain resources they own, such as fungible asset or NFT. Intents could be chain easily with this Intent Transaction Builder.

All rust logics have been implemented.

Suggested developer platform support timeline

Section titled “Suggested developer platform support timeline”

All ts-sdk/indexer logics have been implemented. We will see if there’s other ask from other sdk.

Merge the two changes towards the end of October 2024. We could then release a new version typescript sdk.