Skip to main content

Your First Move Module

This tutorial details how to write, compile, test, publish and interact with Move Modules on the Aptos Blockchain. The steps are:

  1. Write, compile, and test the Move Module
  2. Publish the Move Module to the Aptos Blockchain
  3. Initialize and interact with resources of the Move Module

This tutorial builds on Your first transaction as a library for this example. The following tutorial contains example code that can be downloaded in its entirety below:

For this tutorial, will be focusing on hello_blockchain.ts and re-using the first_transaction.ts library from the previous tutorial.

You can find the typescript project here

Step 1) Write and test the Move Module

Step 1.1) Download Aptos-core

For the simplicity of this exercise, Aptos-core has a move-examples directory that makes it easy to build and test Move modules without downloading additional resources. Over time, we will expand this section to describe how to leverage Move tools for development.

For now, download and prepare Aptos-core:

git clone https://github.com/aptos-labs/aptos-core.git
cd aptos-core
./scripts/dev_setup.sh
source ~/.cargo/env
git checkout origin/devnet

Install Aptos Commandline tool. Learn more about the Aptos command line tool

cargo install --git https://github.com/aptos-labs/aptos-core.git aptos

Step 1.2) Review the Module

In this terminal, change directories to aptos-move/move-examples/hello_blockchain. Keep this terminal window for the rest of this tutorial- we will refer to it later as the "Move Window". The rest of this section will review the file sources/HelloBlockchain.move.

This module enables users to create a String resource under their account and set it. Users are only able to set their resource and cannot set other's resources.

module HelloBlockchain::Message {
use std::string;
use std::error;
use std::signer;

struct MessageHolder has key {
message: string::String,
}

public entry fun set_message(account: signer, message_bytes: vector<u8>)
acquires MessageHolder {
let message = string::utf8(message_bytes);
let account_addr = signer::address_of(&account);
if (!exists<MessageHolder>(account_addr)) {
move_to(&account, MessageHolder {
message,
})
} else {
let old_message_holder = borrow_global_mut<MessageHolder>(account_addr);
old_message_holder.message = message;
}
}
}

In the code above, the two important sections are the struct MessageHolder and the function set_message. set_message is a script function allowing it to be called directly by transactions. Upon calling it, the function will determine if the current account has a MessageHolder resource and creates and stores the message if it does not exist. If the resource exists, the message in the MessageHolder is overwritten.

Step 1.3) Testing the Module

Move allows for inline tests, so we add get_message to make retrieving the message convenient and a test function sender_can_set_message to validate an end-to-end flow. This can be validated by running cargo test. There is another test under sources/HelloBlockchainTest.move that demonstrates another method for writing tests.

This can be tested by entering cargo test test_hello_blockchain -p move-examples -- --exact at the terminal.

Note: sender_can_set_message is a script function in order to call the script function set_message.

    const ENO_MESSAGE: u64 = 0;

public fun get_message(addr: address): string::String acquires MessageHolder {
assert!(exists<MessageHolder>(addr), Errors::not_published(ENO_MESSAGE));
*&borrow_global<MessageHolder>(addr).message
}

#[test(account = @0x1)]
public(script) fun sender_can_set_message(account: signer) acquires MessageHolder {
let addr = Signer::address_of(&account);
set_message(account, b"Hello, Blockchain");

assert!(
get_message(addr) == string::utf8(b"Hello, Blockchain"),
0
);
}

Step 2) Publishing and Interacting with the Move Module

Now we return to our application to deploy and interact with the module on the Aptos blockchain. As mentioned earlier, this tutorial builds upon the earlier tutorial and shares the common code. As a result, this tutorial only discusses new features for that library including the ability to publish, send the set_message transaction, and reading MessageHolder::message. The only difference from publishing a module and submitting a transaction is the payload type. See the following:

Step 2.1) Publishing the Move Module

const client = new AptosClient(NODE_URL);
const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);

/** Publish a new module to the blockchain within the specified account */
export async function publishModule(accountFrom: AptosAccount, moduleHex: string): Promise<string> {
const moudleBundlePayload = new TxnBuilderTypes.TransactionPayloadModuleBundle(
new TxnBuilderTypes.ModuleBundle([new TxnBuilderTypes.Module(new HexString(moduleHex).toUint8Array())]),
);

const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
client.getAccount(accountFrom.address()),
client.getChainId(),
]);

const rawTxn = new TxnBuilderTypes.RawTransaction(
TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
BigInt(sequenceNumber),
moudleBundlePayload,
1000n,
1n,
BigInt(Math.floor(Date.now() / 1000) + 10),
new TxnBuilderTypes.ChainId(chainId),
);

const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);

return transactionRes.hash;
}
tip

To initialize the module, you can write a init_module function. This private function is executed automatically when the module is published. This init_module function must be private, it must only take signer or signer reference as a parameter, and it must not return any value. Here is an example:

 fun init_module(creator: &signer) {
move_to(
creator,
ModuleData { global_counter: 0 }
);
}

Step 2.2) Reading a resource

The module is published at an address. This is the contract_address below. This is similar to the previous example, where the Coin is at 0x1. The contract_address will be the same as the account that publishes it.

/** Retrieve the resource Message::MessageHolder::message */
async function getMessage(contractAddress: HexString, accountAddress: MaybeHexString): Promise<string> {
try {
const resource = await client.getAccountResource(
accountAddress,
`${contractAddress.toString()}::message::MessageHolder`,
);
return (resource as any).data["message"];
} catch (_) {
return "";
}
}

Step 2.3) Modifying a resource

Move modules must expose script functions for initializing and manipulating resources. The script can then be called from a transaction.

Note: while the REST interface can display strings, due to limitations of JSON and Move, it cannot determine if an argument is a string or a hex-encoded string. So the transaction arguments always assume the latter. Hence, in this example, the message is encoded as a hex-string.

/**  Potentially initialize and set the resource Message::MessageHolder::message */
async function setMessage(contractAddress: HexString, accountFrom: AptosAccount, message: string): Promise<string> {
const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(
TxnBuilderTypes.ScriptFunction.natural(
`${contractAddress.toString()}::message`,
"set_message",
[],
[BCS.bcsSerializeStr(message)],
),
);

const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
client.getAccount(accountFrom.address()),
client.getChainId(),
]);

const rawTxn = new TxnBuilderTypes.RawTransaction(
TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
BigInt(sequenceNumber),
scriptFunctionPayload,
1000n,
1n,
BigInt(Math.floor(Date.now() / 1000) + 10),
new TxnBuilderTypes.ChainId(chainId),
);

const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);

return transactionRes.hash;
}

Step 3) Initialize and interact with the Move module

For Typescript:
  • Download the example project
  • Open your favorite terminal and navigate to where you downloaded the above example project
  • Install the required libraries: yarn install
  • Execute the example: yarn hello_blockchain Message.mv
  • After a few moments it will mention that "Update the module with Alice's address, build, copy to the provided path, and press enter."
  • In the "Move Window" terminal, and for the Move file we had previously looked at:
    • Copy Alice's address
    • Compile the modules with Alice's address by aptos move compile --package-dir . --named-addresses HelloBlockchain=0x{alice_address_here}. Here, we replace the generic named address HelloBlockChain='_' in hello_blockchain/move.toml with Alice's Address
    • Copy build/Examples/bytecode_modules/Message.mv to the same folder as this tutorial project code
  • Return to your other terminal window, and press "enter" at the prompt to continue executing the rest of the code

The output should look like the following:

=== Addresses ===
Alice: 11c32982d04fbcc79b694647edff88c5b5d5b1a99c9d2854039175facbeefb40
Bob: 7ec8f962139943bc41c17a72e782b7729b1625cf65ed7812152a5677364a4f88

=== Initial Balances ===
Alice: 10000000
Bob: 10000000

Update the module with Alice's address, build, copy to the provided path, and press enter.

=== Testing Alice ===
Publishing...
Initial value: None
Setting the message to "Hello, Blockchain"
New value: Hello, Blockchain

=== Testing Bob ===
Initial value: None
Setting the message to "Hello, Blockchain"
New value: Hello, Blockchain

The outcome shows that Alice and Bob went from having no resource to one with a message set to "Hello, Blockchain".

The data can be verified by visiting either a REST interface or the explorer: