@stacks/transactions 
Build, sign, and broadcast transactions on the Stacks blockchain. Construct and read Clarity values. Guard transfers with post conditions.
This package works with plain private keys. For app-based flows with a wallet extension, see @stacks/connect. For network configuration (custom nodes, API keys), see @stacks/network.
Installation
npm install @stacks/transactions
Keys and addresses
import {
randomPrivateKey,
privateKeyToPublic,
Address,
} from '@stacks/transactions';
const privateKey = randomPrivateKey(); // hex string
const publicKey = privateKeyToPublic(privateKey);
const mainnetAddress = Address.fromPrivateKey(privateKey); // defaults to mainnet
const testnetAddress = Address.fromPrivateKey(privateKey, 'testnet');
Note: For seed phrases and multi-account wallets, see
@stacks/wallet-sdk.
An address depends on the network. The same key gives a different address on mainnet and testnet.
Send an STX transfer
import {
makeSTXTokenTransfer,
broadcastTransaction,
} from '@stacks/transactions';
const transaction = await makeSTXTokenTransfer({
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: 12345n, // amount in micro-STX
senderKey: privateKey,
network: 'testnet',
memo: 'test memo', // optional
});
const result = await broadcastTransaction({ transaction, network: 'testnet' });
if ('error' in result) throw new Error(result.reason); // or handle
console.log(result.txid);
Note:
networkaccepts the string'mainnet','testnet', or a network object — see@stacks/networkfor custom nodes and API keys.
Things to know:
makeSTXTokenTransfersigns the transaction. Broadcasting is a separatebroadcastTransactioncall.- If you omit
feeornonce, themake*functions fetch them from the network at build time. - The broadcast result is a union. Check
'error' in resultbefore you readresult.txid. - STX transfers cannot carry post conditions.
Call a contract function
import {
makeContractCall,
broadcastTransaction,
Cl,
Pc,
} from '@stacks/transactions';
const transaction = await makeContractCall({
contractAddress: 'SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X',
contractName: 'my-contract',
functionName: 'my-function',
functionArgs: [
// Clarity values — the arguments of the contract function
Cl.uint(100),
Cl.standardPrincipal('SP2ZD731ANQZT6J4K3F5N8A40ZXWXC1XFXHVVQFKE'),
],
senderKey: privateKey,
network: 'mainnet',
postConditions: [
// the network checks these conditions; on a mismatch the transaction aborts
Pc.principal('SP2ZD731ANQZT6J4K3F5N8A40ZXWXC1XFXHVVQFKE')
.willSendGte(1_000_000n)
.ustx(),
],
postConditionMode: 'deny', // the default
});
const result = await broadcastTransaction({ transaction, network: 'mainnet' });
functionArgs takes Clarity values — see Clarity values. postConditions guards asset movement — see Post conditions.
Call a read-only function
A read-only call is a pure API read of the chain state. It builds no transaction and costs no fee. The senderAddress simulates the tx-sender; it needs no key and no funds.
import { fetchCallReadOnlyFunction, Cl } from '@stacks/transactions';
const result = await fetchCallReadOnlyFunction({
contractAddress: 'ST3KC0MTNW34S1ZXD36JYKFD3JJMWA01M55DSJ4JE',
contractName: 'kv-store',
functionName: 'get-value',
functionArgs: [Cl.stringAscii('foo')],
senderAddress: 'ST2F4BK4GZH6YFBNHYDDGN4T1RKBA7DA1BJZPJEJJ',
network: 'testnet',
});
// Narrow on the type — after the check, `value` is correctly typed
if (result.type === 'err') throw new Error('Contract returned an error');
console.log(result.value);
If the contract function returns (err ...), the call still resolves — it does not throw. Branch on result.type to handle contract-level errors.
Clarity values
Contract functions take and return Clarity values. You can construct them in two equal ways: with the Cl namespace, or as plain object literals. Both produce the same values.
import { Cl, type ClarityValue } from '@stacks/transactions';
// Cl namespace
const args = [
Cl.uint(100),
Cl.standardPrincipal('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B'),
Cl.some(Cl.stringUtf8('hello world')),
Cl.tuple({ id: Cl.uint(1), active: Cl.bool(true) }),
];
// Plain literals (identical result)
const sameArgs: ClarityValue[] = [
{ type: 'uint', value: 100n },
{
type: 'address',
value: 'SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B',
},
{
type: 'some',
value: { type: 'utf8', value: 'hello world' },
},
{
type: 'tuple',
value: {
id: { type: 'uint', value: 1n },
active: { type: 'true' },
},
},
];
All Clarity types, in both forms:
| Clarity type | Cl. form |
Plain literal |
|---|---|---|
int |
Cl.int(-100) |
{ type: 'int', value: -100n } |
uint |
Cl.uint(100) |
{ type: 'uint', value: 100n } |
bool |
Cl.bool(true) |
{ type: 'true' } / { type: 'false' } |
buff |
Cl.buffer(bytes), Cl.bufferFromHex('a1b2c3'), Cl.bufferFromAscii(str), Cl.bufferFromUtf8(str) |
{ type: 'buffer', value: 'a1b2c3' } |
string-ascii |
Cl.stringAscii('hi') |
{ type: 'ascii', value: 'hi' } |
string-utf8 |
Cl.stringUtf8('hi') |
{ type: 'utf8', value: 'hi' } |
| standard principal | Cl.standardPrincipal('SP…') |
{ type: 'address', value: 'SP…' } |
| contract principal | Cl.contractPrincipal('SP…', 'name') |
{ type: 'contract', value: 'SP….name' } |
none |
Cl.none() |
{ type: 'none' } |
some |
Cl.some(inner) |
{ type: 'some', value: inner } |
ok |
Cl.ok(inner) |
{ type: 'ok', value: inner } |
err |
Cl.error(inner) |
{ type: 'err', value: inner } |
list |
Cl.list([a, b]) |
{ type: 'list', value: [a, b] } |
tuple |
Cl.tuple({ id: Cl.uint(1) }) |
{ type: 'tuple', value: { id: … } } |
To read a value, narrow on its type and read .value, as shown in the read-only example above.
The Cl namespace also has:
Cl.serialize(value)/Cl.deserialize(hex)— convert a Clarity value to and from wire-format hex, for example atx_result.hexfrom the API.Cl.stringify(value)/Cl.parse(source)— convert a Clarity value to and from Clarity source text, likeJSON.stringifyandJSON.parse.
Post conditions
Post conditions protect users. The network aborts a transaction when its asset movement does not match the declared conditions.
You can construct them in two equal ways: with the Pc builder, or as plain object literals.
import { Pc, Cl, type PostCondition } from '@stacks/transactions';
// Pc builder
const ft = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6')
.willSendGte(2000n)
.ft('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.token-contract', 'my-token');
const nft = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6')
.willSendAsset()
.nft(
'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.nft-contract',
'my-nft',
Cl.uint(1),
);
// Plain literals (identical result)
const conditions: PostCondition[] = [
{
type: 'ft-postcondition',
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
condition: 'gte',
asset: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.token-contract::my-token',
amount: 2000n,
},
{
type: 'nft-postcondition',
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
condition: 'sent',
asset: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.nft-contract::my-nft',
assetId: Cl.uint(1),
},
];
All five post-condition types, in both forms:
| Type | Pc builder |
Plain literal |
|---|---|---|
| STX | Pc.principal(addr).willSendGte(n).ustx() |
{ type: 'stx-postcondition', address, condition: 'gte', amount: n } |
| Fungible token | Pc.principal(addr).willSendEq(n).ft(contractId, tokenName) |
{ type: 'ft-postcondition', address, condition: 'eq', asset: '<contract-id>::<token>', amount: n } |
| Non-fungible token | Pc.principal(addr).willSendAsset().nft(asset, assetId) |
{ type: 'nft-postcondition', address, condition: 'sent', asset, assetId } |
| Staking (SIP-044) | Pc.principal(addr).willSendGte(n).ustxToLock() |
{ type: 'staking-postcondition', address, condition: 'gte', amount: n } |
| PoX (SIP-044) | Pc.principal(addr).willPerformPox() |
{ type: 'pox-postcondition', address, condition: 'will-perform' } |
The comparators per type:
- STX, fungible token, and staking conditions compare an amount:
eq,gt,gte,lt,lte— viawillSendEq,willSendGt,willSendGte,willSendLt,willSendLte. Amounts are in the smallest unit (micro-STX for STX and staking). - Non-fungible token conditions track one token instance:
sent,not-sent,maybe-sent— viawillSendAsset(),willNotSendAsset(),willMaybeSendAsset(). - PoX conditions gate a PoX action and take no amount:
will-perform,will-not-perform,may-perform— viawillPerformPox(),willNotPerformPox(),mayPerformPox()directly on the principal.
Staking and PoX conditions also accept 'origin' as the address, which targets the transaction sender. Pc.origin() produces it:
const staking = Pc.origin().willSendGte(1_000_000n).ustxToLock();
const pox = Pc.origin().willNotPerformPox();
Post-condition mode
The mode controls asset transfers that no condition covers. Declared conditions are ALWAYS checked, in EVERY mode.
'deny'(default) — abort the transaction on any unlisted asset transfer.'allow'— allow unlisted asset transfers.'originator'— deny unlisted transfers for the transaction sender, allow them for others (for example contracts).
postConditionMode: 'deny',
Deploy a contract
import { makeContractDeploy, broadcastTransaction } from '@stacks/transactions';
import { readFileSync } from 'fs';
const transaction = await makeContractDeploy({
contractName: 'my-contract',
codeBody: readFileSync('./contracts/my-contract.clar', 'utf8'),
senderKey: privateKey,
network: 'testnet',
});
const result = await broadcastTransaction({ transaction, network: 'testnet' });
Sign a message
Sign typed, structured data (SIP-018) — for example an off-chain login challenge.
import { Cl, signStructuredData } from '@stacks/transactions';
const domain = Cl.tuple({
name: Cl.stringAscii('my-app'),
version: Cl.stringAscii('1.0.0'),
'chain-id': Cl.uint(1), // 1 = mainnet
});
const message = Cl.tuple({
action: Cl.stringAscii('login'),
nonce: Cl.uint(1),
});
const signature = signStructuredData({ message, domain, privateKey });
The domain tuple must have exactly the fields name, version, and chain-id.
Advanced
Sponsored transactions
A sponsor pays the fee for a transaction that another key signs.
import {
makeContractCall,
sponsorTransaction,
broadcastTransaction,
Cl,
} from '@stacks/transactions';
const originTx = await makeContractCall({
contractAddress: 'SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X',
contractName: 'my-contract',
functionName: 'my-function',
functionArgs: [Cl.bufferFromUtf8('foo')],
senderKey: ORIGIN_PRIVATE_KEY,
sponsored: true, // required for sponsoring
network: 'mainnet',
});
const sponsoredTx = await sponsorTransaction({
transaction: originTx,
sponsorPrivateKey: SPONSOR_PRIVATE_KEY,
network: 'mainnet',
});
const result = await broadcastTransaction({
transaction: sponsoredTx,
network: 'mainnet',
});
Build now, sign later
Build an unsigned transaction with a public key, serialize it, and sign it elsewhere — for example on a hardware device or an air-gapped machine.
import {
makeUnsignedSTXTokenTransfer,
deserializeTransaction,
TransactionSigner,
broadcastTransaction,
} from '@stacks/transactions';
const unsigned = await makeUnsignedSTXTokenTransfer({
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: 12345n,
publicKey, // the unsigned variants take a public key, not a senderKey
network: 'testnet',
});
const hex = unsigned.serialize(); // hex string — transport this
const tx = deserializeTransaction(hex);
const signer = new TransactionSigner(tx);
signer.signOrigin(privateKey);
const result = await broadcastTransaction({
transaction: tx,
network: 'testnet',
});
Fetch helpers
The fetch* functions read from a Stacks node API. They come from this package and take the same network/client options as everything else.
import {
fetchNonce,
fetchFeeEstimate,
fetchAbi,
fetchContractMapEntry,
Cl,
} from '@stacks/transactions';
// the next nonce of an address
const nonce = await fetchNonce({ address, network: 'mainnet' });
// the estimated fee for a built transaction
const fee = await fetchFeeEstimate({ transaction, network: 'mainnet' });
// the interface (ABI) of a contract
const abi = await fetchAbi({
contractAddress: 'SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X',
contractName: 'my-contract',
network: 'mainnet',
});
// one entry of a contract map, without a contract call
const entry = await fetchContractMapEntry({
contractAddress: 'SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X',
contractName: 'my-contract',
mapName: 'my-map',
mapKey: Cl.uint(1),
network: 'mainnet',
});
The make* functions call fetchNonce and fetchFeeEstimate internally when you omit nonce or fee. fetchCallReadOnlyFunction is documented above.
Utilities
transaction.txid()— compute the transaction id of a built transaction.transaction.serialize()/deserializeTransaction(hex)— move a transaction over the wire as hex.validateStacksAddress(address)— check an address string.Address.parse(value)/Address.stringify(repr)— convert between address strings and their parts.