In this tutorial you will learn how to create a dapp on the Aptos blockchain. Usually a dapp consists of a user interface written in JavaScript that interacts with one or more Move modules.
In this tutorial, we will use the Move module HelloBlockchain
described in Your First Move module, and focus on creating the user interface.
We’ll be using:
- Aptos SDK.
- Aptos Wallet, and
- Aptos CLI to interact with the blockchain.
The end result will be a dapp that allows users to publish and share snippets of text on the Aptos blockchain.
The full source code for this tutorial is available here.
Prerequisites
Aptos Wallet.
Before starting this tutorial you need to install the Aptos Wallet extension.
After you install it:
- Open Wallet and click Create New Wallet. Then click Create account to create an Aptos account.
- Copy your private key. You will need it to set up the Aptos CLI in the next section.
NOTE
Make sure you have enough funds in your account for transactions by clicking the Faucet button.
Aptos CLI.
- Install the Aptos CLI.
- Run
aptos init
, and when it asks for your private key, paste the private key from the Aptos Wallet you copied earlier. This will initialize the Aptos CLI to use the same account used in the Aptos Wallet. - Run
aptos account list
to make sure everything works.
Step 1: Create a single-page application
Now we will set up the front-end UI for our dapp. In this tutorial we will use [create-react-app](https://aptos.dev/tutorials/your-first-dapp#:~:text=We%20will%20use-,create%2Dreact%2Dapp,-to%20set%20up)
to customize the app, but neither React nor
create-react-app
are required. You can use your favorite JavaScript framework.
$ npx create-react-app first-dapp --template typescript
$ cd first-dapp
$ npm start
You now have a basic React application running in the browser.
Step 2: Integrate the Web3 Aptos Wallet API
Aptos Wallet provides the Web3 API for dapps in window.aptos
. You can see how it works by opening the browser console and running await window.aptos.account()
. It will display the address that corresponds to the account you created in Aptos Wallet.
Next we'll update our application to use this API to display the wallet account address.
Wait until window.aptos
is defined
The first step in integrating with the window.aptos
API is to wait for the window.onload
event to occur.
Open the src/index.tsx
file and modify the following code snippet:
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
In this:
window.addEventListener('load', () => {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
This change ensures that the window.aptos
API is initialized when the application is rendered (if we render too early, the Wallet extension might not have time to initialize the API yet, so the window.aptos
is undefined
).
(Optional) TypeScript configuration for window.aptos<###code>
If you use TypeScript, you can also tell the compiler that the window.aptos
API exists. Add the following to src/index.tsx
:
declare global {
interface Window { aptos: any; }
}
This allows us to use the window.aptos
API without having to do (window as any).aptos
.
Displaying window.aptos.account()
in the application
Now our application is ready to use the window.aptos.
API We change src/App.tsx
to get the window.aptos.account()
(wallet account) on initial display, save it in state, and then display it:
import React from 'react';
import './App.css';
function App() {
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
}, []);
return (
<div className="App">
<p><code>{ address }</code></p>
</div>
);
}
export default App;
Refresh the page and you will see your account address.
Let's add some CSS
Then replace the content of src/App.css
:
a, input, textarea {
display: block;
}
textarea {
border: 0;
min-height: 50vh;
outline: 0;
padding: 0;
width: 100%;
}
Step 3: Use SDK to retrieve data from the blockchain
The wallet is now integrated into our dapp. Next we'll integrate the Aptos SDK to retrieve data from the blockchain. We will use the Aptos SDK to get our account information and display that information on the page.
Add aptos
to package.json
.
Add the SDK to the project first:
$ npm install --save aptos
You will now see "aptos": "^0.0.20"
(or similar) in your package.json
.
Create AptosClient
.
Now we can import the SDK and create AptosClient
to interact with the blockchain (technically it interacts with the REST API which interacts with the blockchain).
Since our wallet account is in devnet, we will set up AptosClient
to interact with devnet. Add the following to src/App.tsx
:
import { Types, AptosClient } from 'aptos';
// Create an AptosClient to interact with devnet.
const client = new AptosClient('https://fullnode.devnet.aptoslabs.com/v1');
function App() {
// ...
// Use the AptosClient to retrieve details about the account.
const [account, setAccount] = React.useState<Types.AccountData | null>(null);
React.useEffect(() => {
if (!address) return;
client.getAccount(address).then(setAccount);
}, [address]);
return (
<div className="App">
<p><code>{ address }</code></p>
<p><code>{ account?.sequence_number }</code></p>
</div>
);
}
Now, in addition to displaying the account's address, the application will also display sequence_number
of the account. This sequence_number
is the sequence number of the next transaction to prevent transaction replay attacks. You will see this number increase as the account transactions are performed.
Step 4: Publish the Move module
Our dapp is now set up to read from the blockchain. The next step is to write to the blockchain. To do this, we will publish the Move module to our account.
The Move module provides a place to store this data. Specifically, we will use the HelloBlockchain<###code> module from your first Move module, which provides a
MessageHolder
resource that stores a string (called message
).
Publishing the HelloBlockchain
module with the Aptos CLI
We will be using the Aptos CLI to compile and publish the HelloBlockchain
module.
- Download the
hello_blockchain
package. - Then use
aptos move publish
(replace/path/to/hello_blockchain/
and<address>
):
$ aptos move publish --package-dir /path/to/hello_blockchain/ --named-addresses HelloBlockchain=<address>
For example:
$ aptos move publish --package-dir ~/code/aptos-core/aptos-move/move-examples/hello_blockchain/ --named-addresses HelloBlockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481
The --named-addresses
parameter replaces the named address of HelloBlockchain
in HelloBlockchain.move
with the specified address. For example, if we specify --named-addresses HelloBlockchain=0x5af503b5c379bd69f46f418430494975e1ef1fa57f422dd193cdad67dc139d532481
, the following happens:
module HelloBlockchain::Message {
will become:
module 0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message {
This allows you to publish a module for a given account (in this case our Wallet account, 0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481
).
Assuming your account has enough funds to perform the transaction, you can now publish the HelloBlockchain
module to your account. If you refresh the application, you will see that the account sequence number has increased from 0 to 1.
You can also check that the module has been published by going into Aptos Explorer and finding your account. If you scroll down to the "Account Modules" section, you should see something like the following:
{
"address": "0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481",
"name": "Message",
"friends": [],
"exposedFunctions": [
{
"name": "get_message",
"visibility": "public",
"genericTypeParams": [],
"params": [
"address"
],
"_return": [
"0x1::string::String"
]
},
{
"name": "set_message",
"visibility": "script",
"genericTypeParams": [],
"params": [
"signer",
"vector"
],
"_return": []
}
],
"structs": [
{
"name": "MessageChangeEvent",
"isNative": false,
"abilities": [
"drop",
"store"
],
"genericTypeParams": [],
"fields": [
{
"name": "from_message",
"type": "0x1::string::String"
},
{
"name": "to_message",
"type": "0x1::string::String"
}
]
},
{
"name": "MessageHolder",
"isNative": false,
"abilities": [
"key"
],
"genericTypeParams": [],
"fields": [
{
"name": "message",
"type": "0x1::string::String"
},
{
"name": "message_change_events",
"type": "0x1::event::EventHandle<0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message::MessageChangeEvent>"
}
]
}
]
}
Make a note of "name": "Message"
, we'll use it in the next section.
Add instructions for publishing modules to the dapp
For the users' convenience, we can make the application display the aptos move publish
command if the module does not exist. To do this, we'll use the Aptos SDK to get the account modules and look for one where module.abi.name
is "Message"
(i.e. "name": "Message"
which we saw in Aptos Explorer).
Update src/App.tsx
:
function App() {
// ...
// Check for the module; show publish instructions if not present.
const [modules, setModules] = React.useState<Types.MoveModule[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccountModules(address).then(setModules);
}, [address]);
const hasModule = modules.some((m) => m.abi?.name === 'Message');
const publishInstructions = (
<pre>
Run this command to publish the module:
<br />
aptos move publish --package-dir /path/to/hello_blockchain/
--named-addresses HelloBlockchain={address}
</pre>
);
return (
<div className="App">
{!hasModule && publishInstructions}
</div>
);
}
New users will be able to use this command to create a page for their account.
Step 5: Write the message in the blockchain
Now that the module is published, we are ready to use it to write the message in the blockchain. To do this we will use the set_message
function opened by the module.
The transaction calling the set_message
function
The caption for set_message
is as follows:
public(script) fun set_message(account: signer, message_bytes: vector<u8>)
To call this function, we have to use the window.aptos
API provided by the wallet to send the transaction. Specifically, we will create a script_function_payload
transaction that will look like this:
{
type: "script_function_payload",
function: "<address>::Message::set_message",
arguments: ["<hex encoded utf-8 message>"],
type_arguments: []
}
There is no need to specify the account: signer
argument. Aptos provides it automatically.
However we have to specify the message_bytes
argument: it is "<hex encoded utf-8 message>"
in the transaction. We need a way to convert the JS string to this format. We can do this by using TextEncoder
to convert to utf-8 bytes and then a one-line sentence to hexadecimal encode the bytes.
Add this function to src/App.tsx
:
/** Convert string to hex-encoded utf-8 bytes. */
function stringToHex(text: string) {
const encoder = new TextEncoder();
const encoded = encoder.encode(text);
return Array.from(encoded, (i) => i.toString(16).padStart(2, "0")).join("");
}
Using this function, our transaction payload becomes:
{
type: "script_function_payload",
function: "<address>::Message::set_message",
arguments: [stringToHex(message)],
type_arguments: []
}
Use the window.aptos
API to send a set_message
transaction
Now that we understand how to use a transaction to call the set_message
function, we will call this function from our application using window.aptos.signAndSubmitTransaction()
.
We will add:
- Where
<textarea>
the user can enter a message, and
Update src/App.tsx
:
function App() {
// ...
// Call set_message with the textarea value on submit.
const ref = React.createRef<HTMLTextAreaElement>();
const [isSaving, setIsSaving] = React.useState(false);
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!ref.current) return;
const message = ref.current.value;
const transaction = {
type: "script_function_payload",
function: `${address}::Message::set_message`,
arguments: [stringToHex(message)],
type_arguments: [],
};
try {
setIsSaving(true);
await window.aptos.signAndSubmitTransaction(transaction);
} finally {
setIsSaving(false);
}
};
return (
<div className="App">
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} />
<input disabled={isSaving} type="submit" />
</form>
) : publishInstructions}
</div>
);
}
To test:
- Type something in
<textarea>
and submit the form. - Find your account in Aptos Explorer and now you will see
MessageHolder
resource under Account Resources with themessage
you wrote.
If you don't see it, try using a shorter message. Long messages can cause the transaction to fail because long messages take more gas.
Step 6: Displaying the message in dapp
Now that the MessageHolder
resource has been created, we can use the Aptos SDK to fetch it and display the message.
To get a message about the status of a wallet account
To get the message we will do the following:
- First, we'll use the
AptosClient.getAccountResources()
function to get the account resources and store them in state. - Then we will look for the one
type
whichMessageHolder
. The full type is$address::Message::MessageHolder
, since it is part of the$address::Message
module.
In our example it is:
0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message::MessageHolder
- We will use it for the
<textarea>
initial value.
Update src/App.tsx
:
function App() {
// ...
// Get the message from account resources.
const [resources, setResources] = React.useState<Types.AccountResource[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccountResources(address).then(setResourdces);
}, [address]);
const resourceType = `${address}::Message::MessageHolder`;
const resource = resources.find((r) => r.type === resourceType);
const data = resource?.data as {message: string} | undefined;
const message = data?.message;
return (
// ...
<textarea ref={ref} defaultValue={message} />
// ...
);
}
To test:
- Refresh the page and see the message you wrote earlier.
- Change the text, submit the form and refresh the page again. You will see that the page content has been updated with your new message.
This confirms that you are reading and writing messages on the Aptos blockchain.
Displaying messages from other accounts
At this point, we've created a "single-user" dapp where you can read and write messages on your account. Next we'll make it so that other people can read messages, including people who don't have Aptos Wallet installed.
We will set it up so that clicking on the URL /<account address>
will display the message stored at /<account address>
(if it exists).
- If the application is loaded at
/<account address>
, we will also disable editing. - If editing is enabled, we'll show a "Get public URL" link so you can share your post
Update src/App.tsx
:
function App() {
// Retrieve aptos.account on initial render and store it.
const urlAddress = window.location.pathname.slice(1);
const isEditable = !urlAddress;
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
if (urlAddress) {
setAddress(urlAddress);
} else {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
}
}, [urlAddress]);
// ...
return (
<div className="App">
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} defaultValue={message} readOnly={!isEditable} />
{isEditable && (<input disabled={isSaving} type="submit" />)}
{isEditable && (<a href={address!}>Get public URL</a>)}
</form>
) : publishInstructions}
</div>
);
}
This completes this tutorial.