Skip to main content

Transactions

Introduction

Transactions are the fundamental unit of execution in the Stacks blockchain. Each transaction is originated from a Stacks 2.0 account, and is retained in the Stacks blockchain history. This guide helps you understand Stacks 2.0 transactions.

Lifecycle

Transactions go through phases before being finally confirmed, and available for all, on the Stacks 2.0 network.

Transaction lifecycle

  • Generate: Transactions are assembled according to the encoding specification.
  • Validate and sign: Transactions are validated to confirm they are well-formed. Required signatures are filled in.
  • Broadcast: Transactions are sent to a node.
  • Register: A miner receives transactions, verifies, and adds them to the "mempool", a holding area for all the pending transactions.
  • Process: Miners review the mempool and select transactions for the next block to be mined. Depending on the transaction type, different actions can happen during this step. For example, post-conditions could be verified for a token transfer, smart-contract defined tokens could be minted, or an attempt to call an existing smart contract method could be made.
  • Confirm: Miners successfully mine blocks with a set of transactions. The transactions inside are successfully propagated to the network.
info

A transaction can have one of three states once it is registered: pending, success, or failed.

Types

The Stacks 2.0 supports a set of different transaction types:

TypeValueDescription
CoinbasecoinbaseThe first transaction in a new block (an entity holding several transactions). Used to register for block rewards. These are not manually generated and broadcasted like other types of transactions.
Token transfertoken_transferAsset transfer from a sender to a recipient
Contract deploysmart_contractContract instantiation
Contract callcontract_callContract call for a public, non read-only function
Poison Microblockpoison_microblockPunish leaders who intentionally equivocate about the microblocks they package

A sample of each transaction type can be found in the Stacks Blockchain API response definition for transactions.

~> Read-only contract call calls do not require transactions. Read more about it in the network guide.

Anchor mode

Transactions can be mined either in an anchor block or in a microblock. If microblocks are selected, the transaction can be confirmed with a lower latency than the anchor block time.

The anchor mode enum has three options:

  • OnChainOnly The transaction MUST be included in an anchored block
  • OffChainOnly: The transaction MUST be included in a microblock
  • Any: The leader can choose where to include the transaction

Here is an example where the transaction must be included in a microblock:

import { AnchorMode, makeSTXTokenTransfer } from '@stacks/transactions';
import { StacksTestnet, StacksMainnet } from '@stacks/network';

const BigNum = require('bn.js');

const txOptions = {
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: new BigNum(12345),
senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01',
network: new StacksTestnet(), // for mainnet, use `StacksMainnet()`
anchorMode: AnchorMode.OffChainOnly, // must be included in a microblock
};

const transaction = await makeSTXTokenTransfer(txOptions);

Post-conditions

Transaction post-conditions are a feature meant to limit the damage malicious smart contract developers and smart contract bugs can do in terms of destroying a user's assets. Post-conditions are executed whenever a contract is instantiated or a public method of an existing contract is executed. Whenever a post-condition fails, a transaction will be forced to abort.

Post-conditions are meant to be added by the user (or by the user's wallet software) at the moment they sign a transaction. For example, a user may append a post-condition saying that upon successful execution, their account's Stacks (STX) balance should have decreased by no more than 1 STX. If this is not the case, then the transaction would abort and the account would only pay the transaction fee of processing it.

Attributes

Each transaction includes a field that describes zero or more post-conditions that must all be true when the transaction finishes running. The post-condition describes only properties of the owner of the asset before the transaction happened. For a transfer transaction, the post-condition is about the sender, for a burn transaction, the post-condition is about the previous owner. A post-condition includes the following information:

AttributeSampleDescription
PrincipalSP2ZD731ANQZT6J4K3F5N8A40ZXWXC1XFXHVVQFKEoriginal owner of the asset, can be a Stacks address or a contract
Asset idSTXAsset to apply conditions to (could be Stacks, fungible, or non-fungible tokens)
Comparator>=Compare operation to be applied (could define "how much" or "whether or not the asset is owned")
Literal1000000Integer or boolean value used to compare instances of the asset against via the condition

Evaluation modes

The Stacks blockchain supports an allow or deny mode for evaluating post-conditions:

  • Allow: other asset transfers not covered by the post-conditions are permitted
  • Deny: no other asset transfers are permitted besides those named in the post-conditions

Authorization

Transactions can be authorized in two ways: standard and sponsored. The authorization determines whether or not the originating account is also the paying account. In a transaction with a standard authorization, the origin and paying accounts are the same. In a transaction with a sponsored authorization, the origin and paying accounts are distinct, and both accounts must sign the transaction for it to be valid (first the origin, then the spender).

Sponsored transactions enable developers and/or infrastructure operators to pay for users to call into their smart contracts, even if users do not have the Stacks (STX) to do so.

The signing flow for sponsored transactions would be to have the user first sign the transaction with their origin account with the intent of it being sponsored (that is, the user must explicitly allow a sponsor to sign), and then have the sponsor sign with their paying account to pay for the user's transaction fee.

Encoding

A transaction includes the following information. Multiple-byte fields are encoded as big-endian.

TypeDescription
Version numberNetwork version. 0x80 for testnet, 0x0 for mainnet
Chain IDChain instance ID. 0x80000000 for testnet, 0x00000001 for mainnet
AuthorizationType of authorization (0x04 for standard, 0x05 for sponsored) and spending conditions
Post-conditionsList of post-conditions, each including a type ID and variable-length condition body
PayloadTransaction type and variable-length payload

Construction

The easiest way to construct well-formed transactions is by using the Stacks Transactions JS library. You can construct the following transaction types:

  • Stacks token transfer
  • Smart contract deploy
  • Smart contract function call

When constructing transactions, it is required to set the network the transaction is intended for. This can be either mainnet or testnet. At the moment of this writing, the only available option is the testnet network.

info

Transactions can be constructed and serialized offline. However, it is required to know the nonce and estimated fees ahead of time. Once internet access is available, the transaction can be broadcasted to the network. Keep in mind that the nonce and fee might change during offline activity, making the transaction invalid.

Stacks Token transfer

import { makeSTXTokenTransfer } from '@stacks/transactions';
import { StacksTestnet, StacksMainnet } from '@stacks/network';

const BigNum = require('bn.js');

const txOptions = {
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: new BigNum(12345),
senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01',
network: new StacksTestnet(), // for mainnet, use `StacksMainnet()`
memo: 'test memo',
nonce: new BigNum(0), // set a nonce manually if you don't want builder to fetch from a Stacks node
fee: new BigNum(200), // set a tx fee if you don't want the builder to estimate
};

const transaction = await makeSTXTokenTransfer(txOptions);
info

Read more about nonces in the network guide

Smart contract deployment

import { makeContractDeploy } from '@stacks/transactions';
import { StacksTestnet, StacksMainnet } from '@stacks/network';
const BigNum = require('bn.js');

const txOptions = {
contractName: 'contract_name',
codeBody: fs.readFileSync('/path/to/contract.clar').toString(),
senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01',
network: new StacksTestnet(), // for mainnet, use `StacksMainnet()`
};

const transaction = await makeContractDeploy(txOptions);

Smart contract function call

import { makeContractCall, BufferCV } from '@stacks/transactions';
import { StacksTestnet, StacksMainnet } from '@stacks/network';

const BigNum = require('bn.js');

const txOptions = {
contractAddress: 'SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X',
contractName: 'contract_name',
functionName: 'contract_function',
functionArgs: [bufferCVFromString('foo')],
senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01',
// attempt to fetch this contracts interface and validate the provided functionArgs
validateWithAbi: true,
network: new StacksTestnet(), // for mainnet, use `StacksMainnet()`
};

const transaction = await makeContractCall(txOptions);

Clarity value types

Building transactions that call functions in deployed clarity contracts requires you to construct valid Clarity Values to pass to the function as arguments. The Clarity type system contains the following types:

TypeDeclarationDescription
Tuple(tuple (key-name-0 key-type-0) ...)Typed tuple with named fields
List(list max-len entry-type)List of maximum length max-len, with entries of type entry-type
Response(response ok-type err-type)Object used by public functions to commit their changes or abort. May be returned or used by other functions as well, however, only public functions have the commit/abort behavior
Optional(optional some-type)Option type for objects that can either be (some value) or none
Buffer(buff max-len)Byte buffer with maximum length max-len
PrincipalprincipalObject representing a principal (whether a contract principal or standard principal)
BooleanboolBoolean value ('true or 'false)
Signed IntegerintSigned 128-bit integer
Unsigned IntegeruintUnsigned 128-bit integer
ASCII String(define-data-var my-str (string-ascii 11) "hello world")String value encoded in ASCII
UTF-8 String(define-data-var my-str (string-utf8 7) u"hello \u{1234}")String value encoded in UTF-8

The Stacks Transactions JS library contains TypeScript types and classes that map to the Clarity types, in order to make it easy to construct well-typed Clarity values in JavaScript. These types all extend the abstract class ClarityValue.

Here are samples for Clarity value constructions using this library:

// construct boolean clarity values
const t = trueCV();
const f = falseCV();

// construct optional clarity values
const nothing = noneCV();
const something = someCV(t);

// construct a buffer clarity value from an existing Buffer
const buffer = Buffer.from('foo');
const bufCV = bufferCV(buffer);

// construct signed and unsigned integer clarity values
const i = intCV(-10);
const u = uintCV(10);

// construct principal clarity values
const address = 'SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B';
const contractName = 'contract-name';
const spCV = standardPrincipalCV(address);
const cpCV = contractPrincipalCV(address, contractName);

// construct response clarity values
const errCV = responseErrorCV(trueCV());
const okCV = responseOkCV(falseCV());

// construct tuple clarity values
const tupCV = tupleCV({
a: intCV(1),
b: trueCV(),
c: falseCV(),
});

// construct list clarity values
const l = listCV([trueCV(), falseCV()]);

If you develop in Typescript, the type checker can help prevent you from creating wrongly typed Clarity values. For example, the following code won't compile since in Clarity lists are homogeneous, meaning they can only contain values of a single type. It is important to include the type variable BooleanCV in this example, otherwise the typescript type checker won't know which type the list is of and won't enforce homogeneity.

const l = listCV < BooleanCV > [trueCV(), intCV(1)];

Setting post-conditions

The Stacks Transactions JS library supports the construction of post-conditions.

Here is an example of a post-condition that ensures the account's balance will only decrease by no more than 1 STX:

const account = 'SP2ZD731ANQZT6J4K3F5N8A40ZXWXC1XFXHVVQFKE';
const comparator = FungibleConditionCode.GreaterEqual;
// assuming the Stacks (STX) balance before the transaction is 12346
const amount = new BigNum(12345);

const standardSTXPostCondition = makeStandardSTXPostCondition(
account,
comparator,
amount
);

const txOptions = {
..., // other transaction options
postConditions: [standardSTXPostCondition]
}

const transaction = await makeContractCall(txOptions);

Serialization

A well-formed transaction construct is encoded in Recursive Length Prefix ("RLP"). RLP encoding results in a variable-sized byte array.

In order to broadcast transactions to and between nodes on the network, RLP data is represented in hexadecimal string (also called the raw format).

To support an API-friendly and human-readable representation, the Stacks Blockchain API converts transactions into a JSON format.

=> The Stacks Transactions JS library supports serialization of transactions.

Raw format

Broadcasting transactions directly to the Stacks Blockchain API or Node RPC API requires the transaction to be serialized and in hexadecimal representation.

// to see the raw serialized tx
const serializedTx = transaction.serialize().toString('hex');

console.log(serializedTx);

The preceding method returns the following string:

8080000000040015c31b8c1c11c515e244b75806bac48d1399c77500000000000000000000000000000000000127e88a68dce8689fc94ff4c186bf8966f8d544c5129ff84d95a2459b5e8e7c39430388f6c8f85cce8c9ce5e6ec1e157116ca4a67d65ab53768b25d5fb5831939030200000000000516df0ba3e79792be7be5e50a370289accfc8c9e03200000000000f424068656c6c6f20776f726c640000000000000000000000000000000000000000000000

-> Transaction IDs are generated by hashing the raw transaction with sha512/256

JSON format

When called the Stacks Blockchain API or Node RPC API, transactions returned will be serialized in a JSON format. Here is a token transfer transaction:

{
"tx_id": "0x77cb1bf0804f09ad24b4c494a6c00d5b10bb0afbb94a0d646fa9640eff338e37",
"nonce": 5893,
"fee_rate": "180",
"sender_address": "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6",
"sponsored": false,
"post_condition_mode": "deny",
"post_conditions": [],
"anchor_mode": "any",
"block_hash": "0xf1e54a3acd04232f1362c09d5096b095363158348303396ea5fc5092e1d8788f",
"parent_block_hash": "0x3de356eb5afa5d7b781f6a925d31d69d218b772ec995930b4e15d92bd15443f9",
"block_height": 13984,
"burn_block_time": 1622678407,
"burn_block_time_iso": "2021-06-03T00:00:07.000Z",
"canonical": true,
"tx_index": 2,
"tx_status": "success",
"tx_result": {
"hex": "0x0703",
"repr": "(ok true)"
},
"microblock_hash": "",
"microblock_sequence": 2147483647,
"microblock_canonical": true,
"event_count": 1,
"events": [],
"tx_type": "token_transfer",
"token_transfer": {
"recipient_address": "STZ4C5RT4WH4JGRQA5E0ZF5PPSQCVY1WRB6E2CGW",
"amount": "500000000",
"memo": "0x46617563657400000000000000000000000000000000000000000000000000000000"
}
}

Deserializing

Serialized, raw transactions can be deserialized without access to the internet using the Stacks Transactions JS library:

import { BufferReader, deserializeTransaction } from '@stacks/transactions';

// receive raw transaction
const serializedTx = '808000000...';

const bufferReader = new BufferReader(Buffer.from(serializedTx));
const deserializedTx = deserializeTransaction(bufferReader);

// print memo
console.log(deserializedTx.payload.memo.content);

Signature and Verification

Every transaction contains verifiable signatures that certify its authenticity. These signatures are generated by signing the transaction hash with the origin's private key. The Elliptic Curve Digital Signature Algorithm (ECDSA) is used for signing, with the curve set to secp256k1. The internal structure that encapsulates the signature is the spending condition. Spending conditions include several parameters including the public key hash, nonce, fee rate and the recoverable ECDSA signature.

When constructing a transaction using the JS library, you can supply the private key and signing will be completed automatically. If you would like to sign the transaction manually, use the TransactionSigner class.

Below are the steps taken to generate the signature internal to the transaction library.

Signing steps

Step 1: Generate a transaction hash for signing. This is the SHA512/256 digest of the serialized transaction before a signature is added.

Step 2: Append the authorization type, fee amount and nonce to the transaction hash to create the signature hash.

Step 3: Generate the SHA512/256 hash of the resulting string from the previous step.

Step 4: Sign the hash using ECDSA and the origin private key.

Step 5: Add the resulting recoverable ECDSA signature to the transaction spending condition.

Single signature transaction

As the name implies a single signature transaction contains 1 signature from the origin account that authorizes a token spend or smart contract deploy/execution.

Multi-signature transaction

For multi-sig accounts, multiple keys must sign the transaction for it to be valid.

A sponsored transaction is one where a second signer sets and pays the transaction fees. The origin must sign the transaction first before the sponsor signs.

Broadcast

With a serialized transaction in the raw format, it can be broadcast to the network using the POST /v2/transactions endpoint:

# for mainnet, replace `testnet` with `mainnet`
curl --location --request POST 'https://api.testnet.hiro.so/v2/transactions' \
--header 'Content-Type: application/octet-stream' \
--data-raw '<tx_raw_format>'

The API will respond with a HTTP 200 - OK if the transaction was successfully added to the mempool.

There is no explicit time constraint between the construction of a valid signed transaction and when it can be broadcast. There are, however, some constraints to be aware of. The following reasons can deem a transaction invalid after some period:

  • Token transfer: Nonce changed in-between construction and broadcast
  • Contract call or deploy: Block height is evaluated (with at-block) and changed in-between construction and broadcast

Mempool

Once a transaction has been successfully broadcast to the network, the transaction is added to the mempool of the node that received the broadcast. From the Bitcoin wiki: "a node's memory pool contains all 0-confirmation transactions across the entire network that that particular node knows about." So, the set of transactions in the mempool might be different for each node in the network. For example, when you query the mempool endpoints on api.mainnet.hiro.so, the response reflects the set of unconfirmed transactions known to the nodes that service that API.

Miners can employ different heuristics and strategies for deciding which transactions to admit into the mempool and which transactions to include from the mempool when mining a block. Some transactions may be rejected outright (for example, if there are insufficient funds at an address) while others might be accepted into the mempool, but not mined into a block indefinitely (for example if fees are too low). Transactions that are admitted in the mempool but not yet mined are said to be "pending." The current implementation of stacks-blockchain discards pending mempool transactions after 256 blocks.

Best practices

  • Nonce: it's crucial that transactions use the correct nonce. Using an incorrect nonce makes it less likely that the transaction is mined in a timely manner. To determine the correct nonce, query the accounts endpoint of the node you intend to broadcast your transaction to. The value of the nonce field of the response is the next nonce that the node expects to consume for that account. Nonce starts at 0, so the first transaction from an account should be set to nonce=0.
  • Transaction chaining: even when using the correct nonce, transactions might arrive at a node out-of-order. For instance, a transaction with nonce=1 may arrive in the mempool before the nonce=0 transaction. Stacks nodes admit such out-of-order transactions in the mempool, but only up to a limit (25 in the current implementation). So, you should limit any chain of unconfirmed transactions from a single account to less than 25. Making this limit higher has downsides, discussed in this issue. If you need to send more than 25 transactions per block, consider using multiple accounts or a smart-contract based approach. See this tool, for example, that allows up to 200 token transfers in a single transaction.

Querying

Transactions on the Stacks 2.0 network can be queried using the Stacks Blockchain API. The API exposes two interfaces, a RESTful JSON API and a WebSockets API.

For convenience, a Postman Collection was created and published: Run in Postman

-> Note: The API can be easily consumed using a generated JS client library. The generator uses an OpenAPI specification and supports other languages and frameworks.

@include "stacks-api-pagination.md"

Get recent transactions

Recent transactions can be obtained through the GET /extended/v1/tx endpoint:

# for mainnet, replace `testnet` with `mainnet`
curl 'https://api.testnet.hiro.so/extended/v1/tx'

Sample response:

{
"limit": 10,
"offset": 0,
"total": 101922,
"results": [
{
"tx_id": "0x5e9f3933e358df6a73fec0d47ce3e1062c20812c129f5294e6f37a8d27c051d9",
"tx_status": "success",
"tx_type": "coinbase",
"fee_rate": "0",
"sender_address": "ST3WCQ6S0DFT7YHF53M8JPKGDS1N1GSSR91677XF1",
"sponsored": false,
"post_condition_mode": "deny",
"block_hash": "0x58412b50266debd0c35b1a20348ad9c0f17e5525fb155a97033256c83c9e2491",
"block_height": 3231,
"burn_block_time": 1594230455,
"canonical": true,
"tx_index": 0,
"coinbase_payload": {
"data": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}

Get mempool transactions

Mempool (registered, but not processed) transactions can be obtained using the GET /extended/v1/tx/mempool endpoint:

# for mainnet, replace `testnet` with `mainnet`
curl 'https://api.testnet.hiro.so/extended/v1/tx/mempool'

Sample response:

{
"limit": 96,
"offset": 0,
"total": 5,
"results": [
{
"tx_id": "0xb31df5a363dad31723324cb5e0eefa04d491519fd30827a521cbc830114aa50c",
"tx_status": "pending",
"tx_type": "token_transfer",
"receipt_time": 1598288370,
"receipt_time_iso": "2020-08-24T16:59:30.000Z",
"fee_rate": "180",
"sender_address": "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6",
"sponsored": false,
"post_condition_mode": "deny",
"token_transfer": {
"recipient_address": "ST1GY25DM8RZV4X15X07THRZ2C5NMWPGQWKFGV87F",
"amount": "500000",
"memo": "0x46617563657400000000000000000000000000000000000000000000000000000000"
}
}
]
}

-> The memo field is represented as a hexadecimal string of a byte buffer

Filter by type

Recent transactions can be filtered by transaction type using the type query parameter:

# for mainnet, replace `testnet` with `mainnet`
curl 'https://api.testnet.hiro.so/extended/v1/tx/?type=contract_call'

Get transaction by ID

A specific transaction can be obtained using the GET /extended/v1/tx/<tx_id> endpoint:

# for mainnet, replace `testnet` with `mainnet`
curl 'https://api.testnet.hiro.so/extended/v1/tx/<tx_id>'

Sample response:

{
"limit": 96,
"offset": 0,
"total": 5,
"results": [
{
"tx_id": "0xb31df5a363dad31723324cb5e0eefa04d491519fd30827a521cbc830114aa50c",
"tx_status": "pending",
"tx_type": "token_transfer",
"receipt_time": 1598288370,
"receipt_time_iso": "2020-08-24T16:59:30.000Z",
"fee_rate": "180",
"sender_address": "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6",
"sponsored": false,
"post_condition_mode": "deny",
"token_transfer": {
"recipient_address": "ST1GY25DM8RZV4X15X07THRZ2C5NMWPGQWKFGV87F",
"amount": "500000",
"memo": "0x46617563657400000000000000000000000000000000000000000000000000000000"
}
}
]
}

Garbage Collection

Broadcast transactions stay in the mempool for 256 blocks (~42 hours). If a transaction is not confirmed within that time, it is removed from the mempool.

!> Most transactions stay in the mempool due to nonce issues. If you see a transaction pending for an unusual time, review the nonce of the account and the transaction.

If a transaction is removed from the mempool, the transaction was not processed and no changes were made to the blockchain state.