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

Create AIP for Aptos Script/Intent Builder #448

Merged
merged 5 commits into from
Oct 9, 2024
Merged
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
162 changes: 162 additions & 0 deletions aips/aip-102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
aip: 102
title: Dynamic Transaction Composer
author: @runtian-zhou
discussions-to (*optional): <a url pointing to the official discussion thread>
Status: Draft
last-call-end-date (*optional): <mm/dd/yyyy the last date to leave feedbacks and reviews>
type: Interface, Application
created: 09/24/2024
updated (*optional): <mm/dd/yyyy>
requires (*optional): <AIP number(s)>
---

# AIP-102 - Dynamic Transaction Composer

## Summary


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

### Out of Scope

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.

## High-level Overview

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.

## Impact

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 `FungbileAsset` from their `FungibleStore`.
2. User pass the withdrawed `FungbileAsset` 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 `FungbileAsset`.
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`.

## Alternative solutions

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.

## Specification and Implementation Details


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.

## Reference Implementation

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.

## Testing

Implemented tests in both rust library and ts-sdk.

## Risks and Drawbacks

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

## Security Considerations

See Risk.

## Future Potential

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.


## Timeline

### Suggested implementation timeline

All rust logics have been implemented.

### Suggested developer platform support timeline

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

### Suggested deployment timeline

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


## Open Questions (Optional)