Exploring the Aptos SDK
Previously I have made posts about the create-aptos-dapp
tool and Aptos token standard to immerse myself deeper into the Aptos
ecosystem. To further the learning journey, I am writing about Aptos SDK and how to get started.
The article will explore the Aptos SDK, which developers can use to interact with the Aptos
blockchain. It provides a beginner-friendly guide on how to start using the SDK, including the APIs and libraries that simplify tasks for an Aptos blockchain developer.
The Aptos SDK is installed in the front end of an Aptos application and acts as a bridge that provides functions and functionalities to interact with the Aptos blockchain. The SDK is written in different programming languages. The following are the official SDKs provided by the Aptos team to help with development on its blockchain:
The community also provides an unofficial SWIFT SDK.
This blog post will focus on the TypeScript SDK, the code examples will use TypeScipt.
Prerequisite
The following prerequisite should be met to follow along with the code samples in the blog post:
NodeJS is installed on your machine
Aptos CLI is installed on your machine.
A basic understanding of Typescript.
An understanding of using the command line.
Getting Started with the Aptos SDK
The Typescript SDK is used by the create-aptos-dapp
tool to bootstrap a complete Aptos DApp. We will start by installing the SDK and setting up a sample project to explore the SDK's functionality.
Initiate a new NPM project on the command line by executing the following command:
npm init
After initiating a project, install the TypeScript SDK in the project by running:
npm run @aptos-labs/ts-sdk
After installing the SDK, create a folder to store the smart contract code to deploy on-chain. The smart contract will be a simple contract that will enable us to demonstrate some of the functionalities of the TypeSscript SDK.
Create a new file at move/message/Move.toml
. Copy and paste the following configuration code into it:
[package]
name = "Message"
version = "1.0.0"
authors = ["Osikhena James Oshomah"]
[addresses]
Message = "_"
[dev-addresses]
Message = "0x147"
[dependencies.AptosFramework]
git = "https://github.com/aptos-labs/aptos-core.git"
rev = "mainnet"
subdir = "aptos-move/framework/aptos-framework"
Create a new file at move/message/Message/sources/Message.move
. After completing the steps above, the project directory should look like Figure 1.
Figure 1: Project structure
Copy the following smart contract code into the Message.move
file:
module Message::message {
use std::string::{Self, String};
use std::signer;
struct MessageData has key {
message: String,
}
fun init_module(creator: &signer){
move_to(creator, MessageData {
message: string::utf8(b"Hello from Apollo")
});
}
public entry fun create_user_message (user: &signer, message: String){
move_to(user, MessageData {
message
});
}
public entry fun edit_user_message (creator: &signer, user: &signer, message: String) acquires MessageData {
if (!exists<MessageData>(signer::address_of(creator))){
abort(400)
};
let message_mut = borrow_global_mut<MessageData>(signer::address_of(user));
message_mut.message = message;
}
public entry fun edit_user_message_two (user: &signer, message: String) acquires MessageData {
let message_mut = borrow_global_mut<MessageData>(signer::address_of(user));
message_mut.message = message;
}
#[view]
public fun get_user_message(user_address: address):String acquires MessageData {
let message = borrow_global<MessageData>(user_address).message;
message
}
#[test (creator = @Message, user = @0x122 )]
fun test_contract(creator: &signer, user: &signer) acquires MessageData{
init_module(creator);
let message_one = string::utf8(b"This is not a drill");
let message_two = string::utf8(b"This is not a drill");
create_user_message(user, message_one);
let saved_msg_one = borrow_global<MessageData>(signer::address_of(user)).message;
assert!(saved_msg_one == message_one, 900);
edit_user_message(creator, user, message_two);
let saved_msg_two = borrow_global<MessageData>(signer::address_of(user)).message;
assert!(saved_msg_two == message_two, 901);
}
}
The smart contract creates a MessageData
resource stored in a user account. The smart contract allows the owner of MessageData
resource to edit it. We do this to demo how to write a transaction with the SDK and there’s a view function to demonstrate how to use SDK to retrieve data from the blockchain.
We will compile and deploy the smart contract locally as we run the example project we are building. A utility function will be created to compile the contract.
Create a new file utils.js
inside the project's root directory and copy the following code into it:
import { execSync } from "child_process";
import path from "path";
import fs from "fs";
/**
* A convenience function to compile a package locally with the CLI
* @param packageDir
* @param outputFile
* @param namedAddresses
*/
export function compilePackage(
packageDir,
outputFile,
namedAddresses,
) {
console.log("In order to run compilation, you must have the `aptos` CLI installed.");
try {
execSync("aptos --version");
} catch (e) {
console.log("aptos is not installed. Please install it from the instructions on aptos.dev");
}
const addressArg = namedAddresses.map(({ name, address }) => `${name}=${address}`).join(" ");
// Assume-yes automatically overwrites the previous compiled version, only do this if you are sure you want to overwrite the previous version.
const compileCommand = `aptos move build-publish-payload --json-output-file ${outputFile} --package-dir ${packageDir} --named-addresses ${addressArg} --assume-yes`;
console.log("Running the compilation locally, in a real situation you may want to compile this ahead of time.");
console.log(compileCommand);
execSync(compileCommand);
}
/**
* A convenience function to get the compiled package metadataBytes and byteCode
* @param packageDir
* @param outputFile
* @param namedAddresses
*/
export function getPackageBytesToPublish(filePath) {
// current working directory - the root folder of this repo
const cwd = process.cwd();
// target directory - current working directory + filePath (filePath json file is generated with the prevoius, compilePackage, cli command)
const modulePath = path.join(cwd, filePath);
const jsonData = JSON.parse(fs.readFileSync(modulePath, "utf8"));
const metadataBytes = jsonData.args[0].value;
const byteCode = jsonData.args[1].value;
return { metadataBytes, byteCode };
}
The utils.js
file contains code that will compile the Message.move
smart contract and return the contract byte code and metadata. We want to explore the functionality of the SDK on the created smart contract and to do that, we need to compile and deploy the contract.
Creating an Account and Deploying the Sample Project
You can create a new Aptos account and deploy our sample project using the following script. Create a new index.js
file at the root of your project and copy the following code into the file:
import { Account, Aptos, AptosConfig, Network, NetworkToNetworkName } from "@aptos-labs/ts-sdk";
import { compilePackage, getPackageBytesToPublish } from "./utils.js";
//setup the client for making request
const APTOS_NETWORK = NetworkToNetworkName[Network.DEVNET];
const config = new AptosConfig({ network: APTOS_NETWORK });
const aptos = new Aptos(config);
function compileConract(file_name, namedAddresses, outputFile, address) {
console.log("\n=== Compiling package locally ===");
compilePackage(file_name, outputFile, [{ name: namedAddresses, address }]);
const { metadataBytes, byteCode } = getPackageBytesToPublish(outputFile);
return { metadataBytes, byteCode }
}
async function main() {
const adah = Account.generate();
const jason = Account.generate();
console.log("\n=== Addresses ===");
console.log(`Adah: ${adah.accountAddress.toString()}`);
console.log(`Jason: ${jason.accountAddress.toString()}`);
console.log("funding adah's and jason's test account");
await aptos.fundAccount({
accountAddress: adah.accountAddress,
amount: 100_000_000,
});
await aptos.fundAccount({
accountAddress: jason.accountAddress,
amount: 100_000_000,
});
console.log("finish funding account of adah's and jason's test account");
console.log(`\n=== Publishing Message package to ${aptos.config.network} network ===`);
const { metadataBytes, byteCode } = compileConract("move/message", "Message", "move/message/Message.json", adah.accountAddress)
// Publish Message package to chain
const transaction = await aptos.publishPackageTransaction({
account: adah.accountAddress,
metadataBytes,
moduleBytecode: byteCode,
});
const pendingTransaction = await aptos.signAndSubmitTransaction({
signer: adah,
transaction,
});
console.log(`Publish package transaction hash: ${pendingTransaction.hash}`);
await aptos.waitForTransaction({ transactionHash: pendingTransaction.hash });
console.log("Message contract deployed to Devnet")
}
main();
At the top, we imported packages from the TypeScript SDK, and the functions compilePackage and getPackageBytesToPublish from utils.js
that will compile the smart contract.
We defined a small function compileContract
to modularize the codebase and keep things neat. This function returns the contract bytecode and metadataBytes after compilation.
We define an Aptos
client that connects to the devnet, which t is perfect for testing as it resets every Thursday.
We need accounts to deploy a contract or send transactions to the blockchain. So we created two test accounts, adah
and jason
, and funded with 1 APT
each via the faucet.
After funding the accounts, the contract package is deployed using the publishPackageTransaction
function, passing in one of the created account addresses “adah”
which will be the contract deployer and the contract's byteCode and metadataBytes.
To deploy the contract, execute the following command in the terminal.
node index.js
You will see the terminal message below in Figure 2
on your terminal.
Figure 2: Console messages on deploying Message smart contract
Sending a Transaction With the TypeScript SDK
To send a transaction to a blockchain, the account sending the transaction must have APT
to pay for gas, and an account must sign the transaction.
Copy and paste the function below before the declaration of the main function inside the index.js
file.
async function createUserMessage(user, contract_address, message){
const transaction = await aptos.transaction.build.simple({
sender: user.accountAddress,
data: {
function: `${contract_address}::message::create_user_message`,
functionArguments: [message],
},
});
const senderAuthenticator = aptos.transaction.sign({ signer: user, transaction });
const pendingTxn = await aptos.transaction.submit.simple({ transaction, senderAuthenticator });
return pendingTxn.hash;
}
We build the transaction and pass it to a sender, which is the account responsible for signing and paying for the transaction. The function parameter inside the data object represents the function to be called on the blockchain. We are calling create_user_message
function. The format for calling a function is <contract_address::contract_name::function_name>
.
The functionArguments
key represents the parameter passed to the function. The create_user_message
function accepts two parameters: a signer
and a message of type string. We pass only the message parameter because the Aptos VM
will create the signer and pass it to the function.
After building, the transaction is signed and submitted to the blockchain. The createUserMesssage
function returns the submitted transaction hash.
Let us execute the createUserMessage
function. To do that, copy and paste the code below into the main function in the index.js
file:
console.log("sending transaction to create user message");
const createUserMessageTransactionHash = await createUserMessage(jason, adah.accountAddress, "Welcome to the Zone!!!");
await aptos.waitForTransaction({ transactionHash: createUserMessageTransactionHash });
console.log("finish sending transaction to create a user message");
The createUserMessage
function is passed an account, contract address and a message. The function returns a transaction hash that we await with the function aptos.waitForTransaction.
How to use the Aptos SDK to Retrieve Resources from an Account
The createUserMessage
function we defined above moves a MessageData
resource into an account. Let’s see how we could retrieve that resource using the SDK. Add the code to the main function in the index.js
file.
//get back the transaction we sent
console.log("retrieving the user message");
const resource = await aptos.getAccountResource({
accountAddress: jason.accountAddress,
resourceType: `${adah.accountAddress}::message::MessageData`
});
console.log("resources => ", resource);
The function aptos.getAccountResource
takes two parameters: the accountAddress
is the account from which we want to retrieve the resource, and the resourceType
is the type of resource we are retrieving.
The resourceType parameter follows this format: <contract_address::contract_name::resource_type>
How to Sponsor Transaction using TypeScript SDK
An application developer may choose to sponsor and pay the gas fees for their users to encourage them to use their dapp. This is easy to do with the TypeScript SDK in Aptos.
Copy and paste the code below before the main function:
async function sponsorTransactionEditUserMessageTwo(sponsor, user, message, contract_address){
const transaction = await aptos.transaction.build.simple({
sender: user.accountAddress,
withFeePayer: true,
data: {
function: `${contract_address}::message::edit_user_message_two`,
functionArguments: [message],
},
});
// User signs
const senderSignature = aptos.transaction.sign({ signer: user, transaction });
// Sponsor signs
const sponsorSignature = aptos.transaction.signAsFeePayer({ signer: sponsor, transaction });
// Submit the transaction to chain
const committedTxn = await aptos.transaction.submit.simple({
transaction,
senderAuthenticator: senderSignature,
feePayerAuthenticator: sponsorSignature,
});
console.log(`Submitted transaction for sponsorship: ${committedTxn.hash}`);
return committedTxn.hash
}
sponsorTransactionEditUserMessageTwo
takes three parameters: a sponsor account
, the user account signing the transaction, and a message of type string.
When building a transaction that involves a sponsor, we add the key withFeePayer to the parameters accepted by aptos.transaction.build.simple
.
const transaction = await aptos.transaction.build.simple({
sender: user.accountAddress,
withFeePayer: true,
data: {
function: `${contract_address}::message::edit_user_message_two`,
functionArguments: [message],
},
});
The user signs the transaction in the normal way:
const senderSignature = aptos.transaction.sign({ signer: user, transaction });
While the sponsor who pays the gas fee signs as a signAsFeePayer
const sponsorSignature = aptos.transaction.signAsFeePayer({ signer: sponsor, transaction });
The transaction is submitted on-chain with the sender's authenticator (e.g., the senderSignature) and the sponsor's authenticator (e.g., the sponsorSignature). The gas fee will be taken from the sponsor's account.
const committedTxn = await aptos.transaction.submit.simple({
transaction,
senderAuthenticator: senderSignature,
feePayerAuthenticator: sponsorSignature
});
Add the code to the main function of the index.js
file and execute by running node index.js
at the terminal.
console.log("start transaction with a sponsor");
const sponsorTransactionHash = await sponsorTransactionEditUserMessageTwo(adah, jason, "This is a goodday from space", adah.accountAddress)
await aptos.waitForTransaction({ transactionHash: sponsorTransactionHash });
console.log("finish running sponshorship transaction");
Using the Aptos SDK to Get Data From a View Function
A view function returns data from the blockchain. Let’s see how to read data from our smart contract get_user_message
view function. Copy and paste the code below before the main function in index.js
and execute it by running node index.js
async function viewMessageData(userAddress, contract_address) {
const data = await aptos.view({
payload: {
function: `${contract_address}::message::get_user_message`,
functionArguments: [userAddress],
}
});
return data
}
An array is returned from aptos.view
function. Your data from the view function is in the first index of the returned array.
Call the viewMessageData
from the main function passing in an accountAddress
and contract_address
. The contract_address
is where the contract is deployed and we retrieve the message from the accountAddress
parameter.
const data = await viewMessageData(jason.accountAddress, adah.accountAddress);
console.log("data => ", data[0]);
Performing a Multi Signer Transaction with SDK
A multi-signer transaction involves more than one account signer representing an authenticated account. When you sign a transaction, the VM creates a signer type available in your smart contract code.
There might be scenarios when your code requires the action of multiple signers to perform a logic. Like for example a multi-sig wallet. Aptos SDK makes it easy to implement multiple signer transactions. Let’s see how to implement this.
Look at the smart contract code: edit_user_message
. It accepts two signers, a creator, and the user, to update their MessageData
state. Copy and paste the code below before the main function of index.js
:
async function editUserMessage(creator, user, message, contract_address) {
const transaction = await aptos.transaction.build.multiAgent({
sender: user.accountAddress,
secondarySignerAddresses: [creator.accountAddress],
data: {
function: `${contract_address}::message::edit_user_message`,
functionArguments: [message],
},
});
const userSenderAuthenticator = aptos.transaction.sign({
signer: user,
transaction,
});
const creatorSenderAuthenticator = aptos.transaction.sign({
signer: creator,
transaction,
});
const committedTransaction = await aptos.transaction.submit.multiAgent({
transaction,
senderAuthenticator: userSenderAuthenticator,
additionalSignersAuthenticators: [creatorSenderAuthenticator],
});
The transaction is built using the function aptos.transaction.build.multiAgent
instead of aptos.transaction.build.simple
, and it accepts an array of secondarySignerAddress
which contains a number of the other addresses that will sign the transaction.
The transaction is signed by all parties and submitted via aptos.transaction.submit.multiAgent
function to the blockchain.
The function aptos.transaction.submit.multiAgent
accepts an array of additionalSignersAuthenticators
, which are the additional signers
of the transaction.
console.log("sending multi signer tranaction to edit the user message");
const editUserMessageTransactionHash = await editUserMessage(adah, jason, "Goodbye from the Moon. Welcome to earth", adah.accountAddress);
await aptos.waitForTransaction({ transactionHash: editUserMessageTransactionHash });
console.log("finish multi signer tranaction");
Paste the code below inside the main function in index.js
:
Execute the index.js file
by running the following command:
node index.js
Your console will look similar to the messages in Figure 3
.
Figure 3: Console message
Conclusion
From the blog post, you can easily deduce how easy it is to use the TypeScript SDK regardless of your developer experience level. We have barely covered all there is to the TypeScript SDK, but this should serve as a head start as you explore it and its functionalities.
You can download the code for this post from Github.
Thank you for reading!!
Subscribe to my newsletter
Read articles from Osikhena Oshomah directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Osikhena Oshomah
Osikhena Oshomah
Javascript developer working hard to master the craft.