Aptos Tutorial Episode 3: Deploy Things (and, boy, did those things just get easier)

by: Wayne Culbreth

Learning Objectives: Aptos CLI, REST API

This episode is pretty light on new code. In fact, we’re deleting a lot more code than we’re adding. We get to do that because the Aptos team is cranking out devex features faster than you can think to ask for them.

Take a look at this episode’s repo. The first thing you’ll notice: we’ve dropped all those messy Rust files that intrude on our zen of developing in Move. We’re left with a simple directory structure of:

sources└── TicketTutorial.moveMove.toml.gitignore

That’s much more pleasant for my brain. What happened to the other files? Basically, Greg from the Aptos team and the new Aptos CLI happened (with apologies to whoever else worked on this — but I know Greg was cranking out a lot). All of the previous Rust code was just support functions to help us test, compile and ultimately publish our Move modules. That’s all done in the new CLI. We’ll get to that in a minute.

We had one other change to the code from Episode 2. Line 75 of TicketTutorial.move was previously:

TestCoin::transfer_internal(buyer, venue_owner_addr, price);

and we have updated that line to:

TestCoin::transfer(buyer, venue_owner_addr, price);

Previously, script functions did not have the ability to pass a &signer reference — which was problematic for us. The only functions we can call from the REST API are script functions. The previous workaround was to create `transfer_internal` as a regular module function that could accept a &signer reference. Since the time of the last writing, the Aptos team has implemented an update that allows &signer references from script functions — so we can drop the extra ‘transfer_internal’ function.

While this seems like a small change — it’s the process of the change that I’d like to highlight. We are very early in the Aptos/Move journey. The dev team is iterating incredibly fast. If there is a change that the dev community needs — they are cranking out those changes incredibly fast. We’ve all made requests to development teams previously and the typical response is, “Your feature request is noted. We do not comment on potential timelines of new features.” That’s super frustraiting. Scan through the Aptos Discord. When someone makes a feature request, you’re fairly likely to see a response from David or Max that says something like, “Sure — we can probably roll that out by next Thursday.” It’s a different world here.

Let’s get back to the CLI. First off, take a look at the documentation and installation instructions here. You need to have `cargo` installed — those instructions are linked to in the documentation, so I won’t cover them here. You install the CLI simply with:

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

That’s going to churn through some downloads & compiling for a few minutes. Once complete, you can check your installation with:


from your terminal command line, which is going to return the `help` output:

aptos 0.1.0Aptos Labs <opensource@aptoslabs.com>CLI tool for interacting with the Aptos blockchain and nodesUSAGE:    aptos <SUBCOMMAND>OPTIONS:    -h, --help       Print help information    -V, --version    Print version informationSUBCOMMANDS:    account    CLI tool for interacting with accounts    help       Print this message or the help of the given subcommand(s)    init       Tool to initialize current directory for the aptos tool    key        CLI tool for generating, inspecting, and interacting with keys    move       CLI tool for performing Move tasks

Now we’re in business! I’m not going to repeat all the documentation here because it’s actually pretty clear. We are going to walk through how to init, test, compile and deploy our Tickets module using the CLI.

The first thing we need to do is initialize our working directory. Initializing is simply specifying what full node and faucet you want to use and having an account generated & funded for you. Whatever command you run from the CLI is going to use the initialization data in the current folder. To start, run the following command from your terminal:

aptos init

which is going to take you through a series of questions:

Configuring for profile defaultEnter your rest endpoint [Current: None No input: https://fullnode.devnet.aptoslabs.com]

If you just hit <enter> here, you’ll be configured for devnet. If you develop on a local test net like I do, just put in the url:port to your server here (noting that your test net is likely http vs https). Same thing for the faucet which is the next question. The last question asks for a private key, and if you don’t provide one, the CLI will generate one for you. The CLI then does a few things behind the scenes:

  1. Generates a key pair if you didn’t provide one
  2. Creates an account on the provided REST end point with that keypair
  3. Funds the account with 10,000 TestCoin from the provided faucted
  4. Creates a folder in the current directory called “.aptos” and writes a “config.yml” file there with the following content:
---profiles:  default:    private_key: "0xSOMETHING"    public_key: "0x263dfc860812114a4f43985aaf45ee2f0b7e4a8f510f70235188a069f337d7a5"    account: 0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8    rest_url: "https://fullnode.devnet.aptoslabs.com/"    faucet_url: "https://faucet.devnet.aptoslabs.com/"

This configuration file informs everything we do with the CLI in the current working directory. You can run `aptos init` in any folder and create separate configurations. Lets take a look at the account we just created with the command `aptos account list` command:

$ aptos account list{  "Result": [    {      "counter": "2"    },    {      "authentication_key": "0x0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",      "self_address": "0x0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",      "sequence_number": "0"    },    {      "coin": {        "value": "10000"      }    },    {      "received_events": {        "counter": "0",        "guid": {          "guid": {            "id": {              "addr": "0x0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",              "creation_num": "1"            }          },          "len_bytes": 40        }      },      "sent_events": {        "counter": "0",        "guid": {          "guid": {            "id": {              "addr": "0x0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",              "creation_num": "0"            }          },          "len_bytes": 40        }      }    }  ]}

We can quickly see everything about our account and what resources it owns. Very handy tool from the command line. The `aptos account list` can take an optional parameter as `aptos account list — account [YourAccountAddress]` to take a look at a specific address. Otherwise, whenever the account isn’t specified, it’s going to use our default profile.

The beauty of this tool is we can very easily have multiple profiles in the same directory. For our Tickets module, we need a venue owner and we need a customer, so let’s create profiles for each. We can create a named profile by adding a profile argument to init like:

aptos init --profile venue_owner

then do the same for the customer profile:

aptos init --profile customer

So now your `config.yml` file is going to look like this (obviously, your key pairs & addresses will be different than this example. If that’s surprising to you, maybe think about a different line of work):

---profiles:  customer:    private_key: "0xSOMETHING"    public_key: "0xe82f73c9851bdd49f4be541698d316c4e3d8987827b5769bcfb26834c8d8f3ee"    account: 525344e8ef046126664ae8554fcbd73463404a0bcf4328b43a38857bc8b885e3    rest_url: "https://fullnode.devnet.aptoslabs.com/"    faucet_url: "https://faucet.devnet.aptoslabs.com/"  default:    private_key: "0xSOMETHING"    public_key: "0x263dfc860812114a4f43985aaf45ee2f0b7e4a8f510f70235188a069f337d7a5"    account: 0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8    rest_url: "https://fullnode.devnet.aptoslabs.com/"    faucet_url: "https://faucet.devnet.aptoslabs.com/"  venue_owner:    private_key: "0xSOMETHING"    public_key: "0x2a67a14faa7709b045ec89d7340e8a6ef06ca90a8c8ae5362fd1e7f75868cfba"    account: b1b58a7b3d09aa85f87a4ddb60309f75b3d8d47005a55ed5ee6dcd196e7dcd59    rest_url: "https://fullnode.devnet.aptoslabs.com/"    faucet_url: "https://faucet.devnet.aptoslabs.com/"

Now we’ve got three profiles we can work with from the command line. To run a command against a specific profile, just provide the profile argument like:

aptos account list --profile customer

and the CLI will list the contents of that address. Very handy.

Now — we’ve got an important task to do, and one we should always be in the habit of. Currently, our keys are all sitting in `config.yml`. That’s fine, but we don’t want those showing up in a repo anywhere. Yes, this is just a tutorial. Yes, devnet resets every Thursday (for now). No, we aren’t risking any real resources right now. But security is a discipline and we need to always be in the habit of protecting private keys. So, we’re going to modify our `.gitignore` as follows:


which keeps our profile configuration and associated keys from being added to our repo. If you get yourself in the habit of never putting private keys in potentially compromising places, you’ll avoid a host of problems. What starts off as a development key in early versions of an app can sometimes find its way to being a production key later. That in and of itself is a really bad practice — but it’s a bad practice that becomes a nightmare if someone finds that key sitting in an early commit back when “it didn’t matter”. So, build those habits even when just playing around with code. As we used to say in the Army, “train like you fight.”

We’ve got some nice, shiny profiles built and ready to go. Let’s see what else we can do with this CLI. Since we’ve blown away the supporting rust files from our repo, we’re going to do unit testing with the CLI. We can call our ‘sender_can_buy_ticket’ test with:

aptos move test

You’re going to call that command from the directory that contains your Move.toml file. The CLI assumes you’ve put your Move source files in “./sources”. If you have them in any other folder name, you’ll need to tell the CLI that with

aptos move test --package-dir [your_folder]

The tests will run and we then get our same test output like previously:

BUILDING MoveStdlibBUILDING AptosFrameworkBUILDING tutorialsRunning Move unit tests[ PASS    ] 0xe110::Tickets::sender_can_buy_ticketTest result: OK. Total tests: 1; passed: 1; failed: 0{  "Result": "Success"}

We’re about ready to compile our module, but we have to make one change first. As we discussed previously, our Move module is name spaced in the format “Address::Module”. We need to update the address in our Move.toml from “0xe110” to whatever address we generated with “aptos init”. That’s going to be in your “./aptos/config.yml” file. For me — I’m using the default profile as the module owner, so that address in the config.yml file is “default: account:0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8”. I’ve got to copy that address into Move.toml file as:

[addresses]AptosFramework = "0x1"TicketTutorial = "0x0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8

I do note that there is a lack of consistency in when “0x” precedes an address and when it doesn’t. I’ve compiled with the “0x” in front of the address and without — and the compiler seems smart enough to figure it out. We compile the module with:

aptos move compile

which is going to generate our build directory and create the Move bytecode for publishing, with the following confirmation:

{  "Result": [    "0F229AABD5585A4804B38CA4D2557F7CFE5819D257BAB3B94BA7108F8041B2E8::Tickets"  ]}

If you’ve done the Aptos “Your first Move Module” tutorial, you will remember having to copy this file to a folder for deployment. No more of that — the CLI takes care of publishing for us. We can deploy our module simply with:

aptos move publish

We get a nice, detailed confirmation back:

{  "Result": {    "changes": [      {        "address": "0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",        "data": {          "authentication_key": "0x0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",          "self_address": "0xf229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",          "sequence_number": "1"        },        "event": "write_resource",        "resource": "0x1::Account::Account"      },      {        "address": "0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",        "data": {          "coin": {            "value": "9987"          }        },        "event": "write_resource",        "resource": "0x1::TestCoin::Balance"      },      {        "address": "0f229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8",        "event": "write_module"      }    ],    "gas_used": 13,    "success": true,    "version": 618,    "vm_status": "Executed successfully"  }}

Now, there are a few things here you may run into. As of this writing, Move modules cannot be upgraded (although an upgrade path is in the works). What that means is once you publish a module to an address, you cannot publish a new version. If you try to run the publish command again, you’ll get this error:

{  "Error": "API error: transaction execution failed: Transaction Executed and Committed with Error DUPLICATE_MODULE_NAME"}

Now, this can be a bit of a pain if you are in the middle of development and debugging after publishing. If you are deploying to devnet, your only real option is to delete the default account from the config.yml file and running “aptos init” again. This will generate a new address (which you’ll have to also change in your Move.toml file) — but you’ll be able to publish again. Your other option is to deploy to a local test net (which is what I do) — and just restarting the test node from genesis whenever you need to deploy a new version. This restriction is likely temporary in nature as the Aptos team is working on this particular upgrade issue.

We now have a published module, so let’s take it out for a spin. The CLI provides an incredibly useful feature in that we can run functions from our just deployed Move module straight from the command line. The command follows the format:

aptos move run [OPTIONS] --function-id <FUNCTION_ID> --args [type:value]

For options, we can specify which of the named profiles we want to run the command as. We’re going to call the ‘init_venue’ function, so we want to run the command with the ‘venue_owner’ profile. The function ID follows the name space we have already seen: address::module::function. We want to initialize our venue with the `init_venue` function, so the fully qualified function ID would be:


The CLI makes it easier on us, though, and we can forgo the full address and just use “default” making the function ID:


Since the init_venue functions requires us to provide the maximum number of seats, we need to pass some arguments to the function. The arguments aren’t named, they are just taken in order. We’re passing a u64, so for a max seat number of 4, our args would be

--args u64:4

Putting that all together, the command we would run:

aptos move run --profile venue_owner --function-id default::Tickets::init_venue --args u64:4

which gives us an output of:

{  "Result": {    "changes": [      {        "address": "23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",        "data": {          "authentication_key": "0x23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",          "self_address": "0x23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",          "sequence_number": "1"        },        "event": "write_resource",        "resource": "0x1::Account::Account"      },      {        "address": "23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",        "data": {          "coin": {            "value": "9989"          }        },        "event": "write_resource",        "resource": "0x1::TestCoin::Balance"      },      {        "address": "23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",        "data": {          "available_tickets": [],          "max_seats": "4"        },        "event": "write_resource",        "resource": "0xf229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8::Tickets::Venue"      }    ],    "gas_used": 11,    "success": true,    "version": 2819,    "vm_status": "Executed successfully"  }}

We can look in the response and see that, as expected, we have “available_tickets” as an empty vector and “max_seats” as 4. The command line provides us a quick and effective means to test basics about our module immediately upon deployment. Let’s run one more function and create a ticket with:

aptos move run --profile venue_owner --function-id default::Tickets::create_ticket --args string:A16 string:AFHND u64:25

Here, we’re passing arguments of “A16” for the seat number, “AFHND” as the ticket code and a price of 25. Running that command gives us the output:

{  "Result": {    "changes": [      {        "address": "23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",        "data": {          "authentication_key": "0x23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",          "self_address": "0x23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",          "sequence_number": "2"        },        "event": "write_resource",        "resource": "0x1::Account::Account"      },      {        "address": "23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",        "data": {          "coin": {            "value": "9965"          }        },        "event": "write_resource",        "resource": "0x1::TestCoin::Balance"      },      {        "address": "23a555ef96a47503ef6ad4f45087932f6b649bd9f24905df13dc5619fc481185",        "data": {          "available_tickets": [            {              "price": "25",              "seat": "0x413136",              "ticket_code": "0x4146484e44"            }          ],          "max_seats": "4"        },        "event": "write_resource",        "resource": "0xf229aabd5585a4804b38ca4d2557f7cfe5819d257bab3b94ba7108f8041b2e8::Tickets::Venue"      }    ],    "gas_used": 24,    "success": true,    "version": 5168,    "vm_status": "Executed successfully"  }}

Looking at our `available_tickets` vector, we can now see our just created ticket. Note — since the seat and ticket_code values are stored as vector<u8>, we don’t see the actual string, but maybe you’re cool enough to convert hex into ASCII in your head.

One last thing we’ll look at today — and that’s reading data with the REST API. If you recall from the previous episodes, we obtained the number of available tickets by calling `available_ticket_count`. With the REST API, we don’t have to do that. We can pull that information directly from a node via the API call:


where `address` is the account we want to query and `resource_type` is the fully qualified type name (e.g. address::Module::Type). To access the Venue struct on venue_owner, we can make a simple get request of:


Note — the first address is the account we are querying while the second address points to the module that owns the type. The above get request returns:


This is really helpful when developing the client side — we can directly access the data we need. And because we aren’t going through a transaction — there is no gas fee and it is incredibly fast. I highly encourage you to check out the API documentation here:


Play around with the CLI and the REST API covered here. This is going to greatly speed up your development process. Next episode, we’ll build a simple Typescript API around our Move module and then build a UI on top of that.