Your First Transaction
This tutorial describes, in the following step-by-step approach, how to generate, submit, and verify transactions submitted to the Aptos Blockchain:
- Create a representation of an account.
Each Aptos account has a unique account address. The owner of that account holds the public, private key-pair that maps to the Aptos account address and, in turn, the authentication key stored in that account.
note
See more about Aptos accounts in Accounts.
- Prepare a wrapper around the REST interfaces.
Aptos provides a REST API for interacting with the blockchain. This step prepares wrappers around this API, for retrieving account information, and for constructing a transaction, signing it and submitting the transaction. Alternatively, Typescript SDK can be used to interreact with the blockchain. In this tutorial, we use Typescript SDK for the Typescript example and custom wrappers for Python and Rust.
note
Aptos SDK is preferred when available.
- Prepare a wrapper around the Faucet interface.
Using the Faucet interface at the Aptos devnet, this tutorial code automatically creates an account with the account address 0x1
and funds the account.
- Combine the above wrappers into an application, execute and verify.
Before you start
Make sure you follow the below steps first so you can run the tutorial.
Clone the Aptos repo.
git clone https://github.com/aptos-labs/aptos-core.git
cd
intoaptos-core
directory.cd aptos-core
Checkout the devnet branch using
git checkout --track origin/devnet
.Run the
scripts/dev_setup.sh
Bash script as shown below. This will prepare your developer environment../scripts/dev_setup.sh
Update your current shell environment.
source ~/.cargo/env
With your development environment ready, now you are ready to run this tutorial.
GitHub source
Follow the below links to access the source code for the tutorial:
- Typescript
- Python
- Rust
See the first_transaction.ts
code in the Typescript project of the tutorial.
See the first_transaction.py
code in the Python version of the tutorial.
See the first_transaction/src
code in the Rust project of the tutorial.
Step 1: Create a representation of an account
This step creates the representation of an account. See also Aptos accounts and Creating a Signed Transaction.
- Typescript
- Python
- Rust
/** AptosAccount provides methods around addresses, key-pairs */
import { AptosAccount, TxnBuilderTypes, BCS, MaybeHexString } from "aptos";
class Account:
"""Represents an account as well as the private, public key-pair for the Aptos blockchain."""
def __init__(self, seed: bytes = None) -> None:
if seed is None:
self.signing_key = SigningKey.generate()
else:
self.signing_key = SigningKey(seed)
def address(self) -> str:
"""Returns the address associated with the given account"""
return self.auth_key()
def auth_key(self) -> str:
"""Returns the auth_key for the associated account"""
hasher = hashlib.sha3_256()
hasher.update(self.signing_key.verify_key.encode() + b'\x00')
return hasher.hexdigest()
def pub_key(self) -> str:
"""Returns the public key for the associated account"""
return self.signing_key.verify_key.encode().hex()
pub struct Account {
signing_key: SecretKey,
}
impl Account {
/// Represents an account as well as the private, public key-pair for the Aptos blockchain.
pub fn new(priv_key_bytes: Option<Vec<u8>>) -> Self {
let signing_key = match priv_key_bytes {
Some(key) => SecretKey::from_bytes(&key).unwrap(),
None => {
let mut rng = rand::rngs::StdRng::from_seed(OsRng.gen());
let mut bytes = [0; 32];
rng.fill_bytes(&mut bytes);
SecretKey::from_bytes(&bytes).unwrap()
}
};
Account { signing_key }
}
/// Returns the address associated with the given account
pub fn address(&self) -> String {
self.auth_key()
}
/// Returns the auth_key for the associated account
pub fn auth_key(&self) -> String {
let mut sha3 = Sha3::v256();
sha3.update(PublicKey::from(&self.signing_key).as_bytes());
sha3.update(&vec![0u8]);
let mut output = [0u8; 32];
sha3.finalize(&mut output);
hex::encode(output)
}
/// Returns the public key for the associated account
pub fn pub_key(&self) -> String {
hex::encode(PublicKey::from(&self.signing_key).as_bytes())
}
}
Step 2: REST interface
While the data from the REST interface can be read directly, the following code examples demonstrate a more ergonomic approach, while still using the REST interface, for:
- Retrieving the ledger data from the FullNode, including account and account resource data.
- Constructing signed transactions, represented by JSON format.
- Typescript
- Python
- Rust
/** Wrappers around the Aptos Node and Faucet API */
import { AptosClient, FaucetClient } from "aptos";
class RestClient:
"""A wrapper around the Aptos-core Rest API"""
def __init__(self, url: str) -> None:
self.url = url
#[derive(Clone)]
pub struct RestClient {
url: String,
}
impl RestClient {
/// A wrapper around the Aptos-core Rest API
pub fn new(url: String) -> Self {
Self { url }
}
Step 2.1: Reading an account
The following are wrappers for querying account data.
- Typescript
- Python
- Rust
const client = new AptosClient(NODE_URL);
/**
* https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#getAccount
* returns the sequence number and authentication key for an account
*
* https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#getAccountResource
* returns all resources associated with the account
*/
def account(self, account_address: str) -> Dict[str, str]:
"""Returns the sequence number and authentication key for an account"""
response = requests.get(f"{self.url}/accounts/{account_address}")
assert response.status_code == 200, f"{response.text} - {account_address}"
return response.json()
def account_resource(self, account_address: str, resource_type: str) -> Optional[Dict[str, Any]]:
response = requests.get(f"{self.url}/accounts/{account_address}/resource/{resource_type}")
if response.status_code == 404:
return None
assert response.status_code == 200, response.text
return response.json()
/// Returns the sequence number and authentication key for an account
pub fn account(&self, account_address: &str) -> serde_json::Value {
let res =
reqwest::blocking::get(format!("{}/accounts/{}", self.url, account_address)).unwrap();
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
account_address,
);
}
res.json().unwrap()
}
/// Returns all resources associated with the account
pub fn account_resource(
&self,
account_address: &str,
resource_type: &str,
) -> Option<serde_json::Value> {
let res = reqwest::blocking::get(format!(
"{}/accounts/{}/resource/{}",
self.url, account_address, resource_type,
))
.unwrap();
if res.status() == 404 {
None
} else if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
account_address,
);
unreachable!()
} else {
Some(res.json().unwrap())
}
}
Step 2.2: Submitting a transaction
The following demonstrates the core functionality for constructing, signing, and waiting on a transaction.
- Typescript
- Python
- Rust
/**
* https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateBCSTransaction
* signs a raw transaction, which can be submitted to the blockchain.
*/
/**
* https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#submitSignedBCSTransaction
* submits a signed transaction to the blockchain.
*/
def generate_transaction(self, sender: str, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Generates a transaction request that can be submitted to produce a raw transaction that
can be signed, which upon being signed can be submitted to the blockchain. """
account_res = self.account(sender)
seq_num = int(account_res["sequence_number"])
txn_request = {
"sender": f"0x{sender}",
"sequence_number": str(seq_num),
"max_gas_amount": "2000",
"gas_unit_price": "1",
"expiration_timestamp_secs": str(int(time.time()) + 600),
"payload": payload,
}
return txn_request
def sign_transaction(self, account_from: Account, txn_request: Dict[str, Any]) -> Dict[str, Any]:
"""Converts a transaction request produced by `generate_transaction` into a properly signed
transaction, which can then be submitted to the blockchain."""
res = requests.post(f"{self.url}/transactions/signing_message", json=txn_request)
assert res.status_code == 200, res.text
to_sign = bytes.fromhex(res.json()["message"][2:])
signature = account_from.signing_key.sign(to_sign).signature
txn_request["signature"] = {
"type": "ed25519_signature",
"public_key": f"0x{account_from.pub_key()}",
"signature": f"0x{signature.hex()}",
}
return txn_request
def submit_transaction(self, txn: Dict[str, Any]) -> Dict[str, Any]:
"""Submits a signed transaction to the blockchain."""
headers = {'Content-Type': 'application/json'}
response = requests.post(f"{self.url}/transactions", headers=headers, json=txn)
assert response.status_code == 202, f"{response.text} - {txn}"
return response.json()
def execute_transaction_with_payload(self, account_from: Account, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a transaction for the given payload."""
txn_request = self.generate_transaction(account_from.address(), payload)
signed_txn = self.sign_transaction(account_from, txn_request)
return self.submit_transaction(signed_txn)
def transaction_pending(self, txn_hash: str) -> bool:
response = requests.get(f"{self.url}/transactions/{txn_hash}")
if response.status_code == 404:
return True
assert response.status_code == 200, f"{response.text} - {txn_hash}"
return response.json()["type"] == "pending_transaction"
def wait_for_transaction(self, txn_hash: str) -> None:
"""Waits up to 10 seconds for a transaction to move past pending state."""
count = 0
while self.transaction_pending(txn_hash):
assert count < 10, f"transaction {txn_hash} timed out"
time.sleep(1)
count += 1
response = requests.get(f"{self.url}/transactions/{txn_hash}")
assert "success" in response.json(), f"{response.text} - {txn_hash}"
/// Generates a transaction request that can be submitted to produce a raw transaction that can be signed, which upon being signed can be submitted to the blockchain.
pub fn generate_transaction(
&self,
sender: &str,
payload: serde_json::Value,
) -> serde_json::Value {
let account_res = self.account(sender);
let seq_num = account_res
.get("sequence_number")
.unwrap()
.as_str()
.unwrap()
.parse::<u64>()
.unwrap();
// Unix timestamp, in seconds + 10 minutes
let expiration_time_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
+ 600;
serde_json::json!({
"sender": format!("0x{}", sender),
"sequence_number": seq_num.to_string(),
"max_gas_amount": "1000",
"gas_unit_price": "1",
"expiration_timestamp_secs": expiration_time_secs.to_string(),
"payload": payload,
})
}
/// Converts a transaction request produced by `generate_transaction` into a properly signed transaction, which can then be submitted to the blockchain.
pub fn sign_transaction(
&self,
account_from: &mut Account,
mut txn_request: serde_json::Value,
) -> serde_json::Value {
let res = reqwest::blocking::Client::new()
.post(format!("{}/transactions/signing_message", self.url))
.body(txn_request.to_string())
.send()
.unwrap();
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
txn_request.as_str().unwrap_or(""),
);
}
let body: serde_json::Value = res.json().unwrap();
let to_sign_hex = Box::new(body.get("message").unwrap().as_str()).unwrap();
let to_sign = hex::decode(&to_sign_hex[2..]).unwrap();
let signature: String = ExpandedSecretKey::from(&account_from.signing_key)
.sign(&to_sign, &PublicKey::from(&account_from.signing_key))
.encode_hex();
let signature_payload = serde_json::json!({
"type": "ed25519_signature",
"public_key": format!("0x{}", account_from.pub_key()),
"signature": format!("0x{}", signature),
});
txn_request
.as_object_mut()
.unwrap()
.insert("signature".to_string(), signature_payload);
txn_request
}
/// Submits a signed transaction to the blockchain.
pub fn submit_transaction(&self, txn_request: &serde_json::Value) -> serde_json::Value {
let res = reqwest::blocking::Client::new()
.post(format!("{}/transactions", self.url))
.body(txn_request.to_string())
.header("Content-Type", "application/json")
.send()
.unwrap();
if res.status() != 202 {
assert_eq!(
res.status(),
202,
"{} - {}",
res.text().unwrap_or("".to_string()),
txn_request.as_str().unwrap_or(""),
);
}
res.json().unwrap()
}
/// Submits a signed transaction to the blockchain.
pub fn execution_transaction_with_payload(
&self,
account_from: &mut Account,
payload: serde_json::Value,
) -> String {
let txn_request = self.generate_transaction(&account_from.address(), payload);
let signed_txn = self.sign_transaction(account_from, txn_request);
let res = self.submit_transaction(&signed_txn);
res.get("hash").unwrap().as_str().unwrap().to_string()
}
pub fn transaction_pending(&self, transaction_hash: &str) -> bool {
let res = reqwest::blocking::get(format!("{}/transactions/{}", self.url, transaction_hash))
.unwrap();
if res.status() == 404 {
return true;
}
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
transaction_hash,
);
}
res.json::<serde_json::Value>()
.unwrap()
.get("type")
.unwrap()
.as_str()
.unwrap()
== "pending_transaction"
}
/// Waits up to 10 seconds for a transaction to move past pending state.
pub fn wait_for_transaction(&self, txn_hash: &str) {
let mut count = 0;
while self.transaction_pending(txn_hash) {
assert!(count < 10, "transaction {} timed out", txn_hash);
thread::sleep(Duration::from_secs(1));
count += 1;
}
}
Step 2.3: Application-specific logic
The following demonstrates how to read data from the blockchain and how to submit a specific transaction.
- Typescript
- Python
- Rust
/** Helper method returns the coin balance associated with the account */
export async function accountBalance(accountAddress: MaybeHexString): Promise<number | null> {
const resource = await client.getAccountResource(accountAddress, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>");
if (resource == null) {
return null;
}
return parseInt((resource.data as any)["coin"]["value"]);
}
/**
* Transfers a given coin amount from a given accountFrom to the recipient's account address.
* Returns the transaction hash of the transaction used to transfer.
*/
async function transfer(accountFrom: AptosAccount, recipient: MaybeHexString, amount: number): Promise<string> {
const token = new TxnBuilderTypes.TypeTagStruct(TxnBuilderTypes.StructTag.fromString("0x1::aptos_coin::AptosCoin"));
const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(
TxnBuilderTypes.ScriptFunction.natural(
"0x1::coin",
"transfer",
[token],
[BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(recipient)), BCS.bcsSerializeUint64(amount)],
),
);
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 pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);
return pendingTxn.hash;
}
def account_balance(self, account_address: str) -> Optional[int]:
"""Returns the test coin balance associated with the account"""
return self.account_resource(account_address, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>")
def transfer(self, account_from: Account, recipient: str, amount: int) -> str:
"""Transfer a given coin amount from a given Account to the recipient's account address.
Returns the sequence number of the transaction used to transfer."""
payload = {
"type": "script_function_payload",
"function": "0x1::coin::transfer",
"type_arguments": ["0x1::aptos_coin::AptosCoin"],
"arguments": [
f"0x{recipient}",
str(amount),
]
}
txn_request = self.generate_transaction(account_from.address(), payload)
signed_txn = self.sign_transaction(account_from, txn_request)
res = self.submit_transaction(signed_txn)
return str(res["hash"])
/// Returns the test coin balance associated with the account
pub fn account_balance(&self, account_address: &str) -> Option<u64> {
self.account_resource(
account_address,
"0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
)
.unwrap()["data"]["coin"]["value"]
.as_str()
.and_then(|s| s.parse::<u64>().ok())
}
/// Transfer a given coin amount from a given Account to the recipient's account address.
/// Returns the sequence number of the transaction used to transfer
pub fn transfer(&self, account_from: &mut Account, recipient: &str, amount: u64) -> String {
let payload = serde_json::json!({
"type": "script_function_payload",
"function": "0x1::coin::transfer",
"type_arguments": ["0x1::aptos_coin::AptosCoin"],
"arguments": [format!("0x{}", recipient), amount.to_string()]
});
let txn_request = self.generate_transaction(&account_from.address(), payload);
let signed_txn = self.sign_transaction(account_from, txn_request);
let res = self.submit_transaction(&signed_txn);
res.get("hash").unwrap().as_str().unwrap().to_string()
}
}
Step 3: Faucet interface
Aptos Blockchain faucets issue test tokens to accounts. These test tokens can be used for testing, e.g., paying gas fees or transferring tokens between users. The Aptos Faucet can also create accounts if they do not exist. The Aptos Faucet interface requires a public key represented in a hex-encoded string.
- Typescript
- Python
- Rust
/** Faucet creates and funds accounts. */
const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);
class FaucetClient:
"""Faucet creates and funds accounts. This is a thin wrapper around that."""
def __init__(self, url: str, rest_client: RestClient) -> None:
self.url = url
self.rest_client = rest_client
def fund_account(self, address: str, amount: int) -> None:
"""This creates an account if it does not exist and mints the specified amount of
coins into that account."""
txns = requests.post(f"{self.url}/mint?amount={amount}&address={address}")
assert txns.status_code == 200, txns.text
for txn_hash in txns.json():
self.rest_client.wait_for_transaction(txn_hash)
pub struct FaucetClient {
url: String,
rest_client: RestClient,
}
impl FaucetClient {
/// Faucet creates and funds accounts. This is a thin wrapper around that.
pub fn new(url: String, rest_client: RestClient) -> Self {
Self { url, rest_client }
}
/// This creates an account if it does not exist and mints the specified amount of coins into that account.
pub fn fund_account(&self, auth_key: &str, amount: u64) {
let res = reqwest::blocking::Client::new()
.post(format!(
"{}/mint?amount={}&auth_key={}",
self.url, amount, auth_key
))
.send()
.unwrap();
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{}",
res.text().unwrap_or("".to_string()),
);
}
for txn_hash in res.json::<serde_json::Value>().unwrap().as_array().unwrap() {
self.rest_client
.wait_for_transaction(txn_hash.as_str().unwrap())
}
}
}
Step 4: Run the application
Finally, we can run the application and verify the output.
- Typescript
- Python
- Rust
- Make sure you followed the prerequisites described in Before you start.
cd
intoaptos-core/developer-docs-site/static/examples/typescript
directory.- Install the required libraries:
yarn install
. - Execute the example:
yarn first_transaction
.
- Make sure you followed the prerequisites described in Before you start.
cd
intoaptos-core/developer-docs-site/static/examples/python
directory.- Install the required libraries:
pip3 install -r requirements.txt
. - Run the example:
python3 first_transaction.py
.
- Make sure you followed the prerequisites described in Before you start.
cd
intoaptos-core/developer-docs-site/static/examples/rust
directory.- Execute the example:
cargo run --bin first-transaction
(make sure you usefirst-transaction
and notfirst_transaction
).
Output
The output after executing:
=== Addresses ===
Alice: e26d69b8d3ff12874358da6a4082a2ac
Bob: c8585f009c8a90f22c6b603f28b9ed8c
=== Initial Balances ===
Alice: 5000
Bob: 0
=== Final Balances ===
Alice: 3927
Bob: 1000
The output shows that Bob received 1000 coins from Alice. Alice paid 73 coins for gas.
Verify
The data can be verified by visiting either a REST interface or the explorer:
- Alice's account via the Aptos REST interface.
- Bob's account via the Aptos Explorer.
note
The Aptos devnet is reset from time to time, so the above links may not work. Try the tutorial yourself and check the accounts in the Aptos Explorer then.