Skip to main content

Clarinet SDK

Overview

The Clarinet SDK is a JavaScript library that spawns and interacts with a simulated Clarinet environment, also known as "simnet."

A Simnet is a simulated network that mimics the Stacks blockchain and runs the Clarity VM, without the need for actual Stacks and Bitcoin nodes (unlike Devnet, Testnet and Mainnet).

Here is a non-exhaustive list of some of simnet's use-cases:

Getting Started With the SDK

The SDK requires Node.js >= 18.0 and NPM to be installed. Volta is a great tool to install and manage JS tooling.

The SDK can be installed with NPM. It works in pair with Stacks.js, so let's install it as well.

npm install @hirosystems/clarinet-sdk @stacks/transactions

Usage

Here is a very basic code snippet showing how to use the SDK:

import { initSimnet } from '@hirosystems/clarinet-sdk';
import { Cl } from '@stacks/transactions';

async function main() {
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1');
if (!address1) throw new Error('invalid wallet name.');

const call = simnet.callPublicFn('counter', 'add', [Cl.uint(1)], address1);
console.log(call.result); // Cl.int(Cl.ok(true))
}

main();

By default, the SDK will look for a Clarinet.toml file in the current working directory. It's also possible to provide the path to the manifest like so:

const simnet = await initSimnet('./path/to/Clarinet.toml');

API References

initSimnet

initSimnet(manifestPath?: string): Promise<Simnet>

The initSimnet function takes the manifest path (Clarinet.toml) as an optional argument. By default, it'll look for a manifest in the current working directory. It will often be the first function to call when using the SDK.

import { initSimnet } from '@hirosystems/clarinet-sdk';

const simnet = await initSimnet();
// or
const simnet = await initSimnet('./clarity/Clarinet.toml');

Simnet Properties

Simnet.blockHeight

Returns the current block height of the simnet.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

console.log(simnet.blockHeight); // 0

Simnet.deployer

Returns the default deployer address as defined in the project file ./setting/Devnet.toml.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

console.log(simnet.deployer); // ST1P...GZGM

Simnet Methods

Simnet.getAccounts()

getAccounts(): Map<string, string>

Get the Stacks addresses defined in the project file ./setting/Devnet.toml.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;
console.log(address1); // ST1S...YPD5

Simnet.getAssetsMap()

getAssetsMap(): Map<string, Map<string, bigint>>

Get a list of asset balances by Stacks addresses. This method returns STX balances as well as FT and NFT balances.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const assets = simnet.getAssetsMap();
const stxBalances = assets.get('STX')!;

console.log(stxBalances);
// Map(10) {
// 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM' => 100000000000000n,
// 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5' => 100000000000000n,
// // ...
// }

Simnet.getDataVar()

getDataVar(contract: string, dataVar: string): ClarityValue

Get the value of a data-var defined in a contract.

Given a contract with the following definition:

(define-data-var count uint u0)

It can be accessed with:

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const counter = simnet.getDataVar('counter', 'count');
// counter is Cl.uint(0)

Simnet.getMapEntry()

getMapEntry(contract: string, mapName: string, mapKey: ClarityValue): ClarityValue

Get the value of a map entry by its key. Note that it will always return an optional value ((some <value>) or none). Just like Clarity map-get?.

Given a contract with the following definition:

(define-map participants principal bool)

It can be accessed with:

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;

const participated = simnet.getMapEntry('counter', 'participants', Cl.standardPrincipal(address1));
// counter is Cl.some(Cl.bool(true|false)) or Cl.none()

Simnet.callReadOnlyFn()

callReadOnlyFn(
contract: string, // stacks address of the contract
method: string, // read-only function to call
args: ClarityValue[], // array of Clarity Values
sender: string // stacks address of the sender
): ParsedTransactionRes

Call read-only functions exposed by a contract. This method returns an object with the result of the function call as a Clarity Value. It takes function arguments in the form in Clarity Values, available in the package @stacks/transactions.

import { initSimnet } from '@hirosystems/clarinet-sdk';
import { Cl } from '@stacks/transactions';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;

const getCounter = simnet.callReadOnlyFn('counter', 'get-counter', [], address1);
console.log(getCounter.result); // Cl.uint(1)

// With arguments:
const callPOX = simnet.callReadOnlyFn('pox-3', 'is-pox-active', [Cl.uint(100)], address1);

As in many methods of the SDK, the contract address can be just the contract name, if deployed by the default deployer.

simnet.callReadOnlyFn('counter', 'get-counter', [], address1);
// equivalent
simnet.callReadOnlyFn(`${simnet.deployer}.counter`, 'get-counter', [], address1);

Simnet.callPublicFn()

callPublicFn(
contract: string, // stacks address of the contract
method: string, // public function to call
args: ClarityValue[], // array of Clarity Values
sender: string // stacks address of the sender
): ParsedTransactionRes

Call read-only functions exposed by a contract. This method returns an object with the result of the function call as a Clarity Value and the events fired during the function execution. It takes function arguments in the form in Clarity Values, available in the package @stacks/transactions. It will simulate a block being mined and increase the block height by one.

import { initSimnet } from '@hirosystems/clarinet-sdk';
import { Cl } from '@stacks/transactions';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;

const callAdd = simnet.callPublicFn('counter', 'add', [Cl.uint(3)], address1);
console.log(callAdd.result); // a Clarity Value such as Cl.bool(true)
console.log(callAdd.events); // and array of events (such as print event, stx stransfer event, etc)

Simnet.transferSTX()

transferSTX(amount: number | bigint, recipient: string, sender: string): ParsedTransactionRes

Transfer STX from an address to an other. The amount is in uSTX. It will simulate a block being mined and increase the block height by one.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;
const address2 = accounts.get('wallet_2')!;

const transfer = simnet.transferSTX(100, address1, address2);
console.log(transfer);
// {
// result: Cl.ok(Cl.bool(true)),
// events: [
// {
// event: 'stx_transfer_event',
// data: {
// amount: '100',
// memo: '',
// recipient: 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5',
// sender: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG'
// }
// }
// ]
// }

Simnet.deployContract()

deployContract(
// name of the contract to be deployed
name: string,
// content of the contract
content: string,
// an object to specify options such as the ClarityVersion
options: DeployContractOptions | null,
// sender stacks address
sender: string
): ParsedTransactionRes

Deploy a contract to the Simnet. It will simulate a block being mined and increase the block height by one.

import { initSimnet } from '@hirosystems/clarinet-sdk';
import { Cl } from '@stacks/transactions';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;

const source = '(define-public (add (a uint) (b uint)) (ok (+ a b)))';
const deployRes = simnet.deployContract('op', source, simnet.deployer);

const addRes = simnet.callPublicFn('op', 'add', [Cl.uint(1), Cl.uint(1)], address1);
console.log(addRes.result); // Cl.ok(Cl.uint(2))

// specify a clarityVersion
simnet.deployContract('contract2', source, { clarityVersion: 2 }, deployerAddr);

Simnet.mineBlock()

mineBlock(txs: Tx[]): ParsedTransactionRes[]

The .callPublicFn(), .transferSTX(), and .deployContract() methods all mine one block with only one transaction. It can also be useful to mine a block with multiple transactions. This is what .mineBlock() is for.

It take an array of transaction objects. The transactions can be built with the tx helper exported by the SDK. It has three methods .callPublicFn(), .transferSTX(), and .deployContract(), which have the same interface as the Simnet methods but instead of performing a transaction, it will build a transaction object than can be passed to the mineBlock() function.

// import `tx` as well
import { initSimnet, tx } from '@hirosystems/clarinet-sdk';
import { Cl } from '@stacks/transactions';
const simnet = await initSimnet();

const accounts = simnet.getAccounts();
const address1 = accounts.get('wallet_1')!;
const address2 = accounts.get('wallet_2')!;

const block = simnet.mineBlock([
tx.callPublicFn('counter', 'increment', [], address1),
tx.callPublicFn('counter', 'add', [Cl.uint(10)], address1),
tx.transferSTX(100, address1, address2),
]);

console.log(block[0]); // `increment` response with { result, events}
console.log(block[1]); // `add` response with { result, events}
console.log(block[2]); // `transfer_stx` response with { result, events}

Simnet.mineEmptyBlock()

mineEmptyBlock(): number

Mine one empty block and increase the block height by one. Returns the new block height.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

console.log(simnet.blockHeight); // 0
const newHeight = simnet.mineEmptyBlock();
cosole.log(newHeight); // 1
console.log(simnet.blockHeight); // 1

Simnet.mineEmptyBlocks()

mineEmptyBlocks(count?: number): number

Mine multiple empty blocks to reach a certain block height. Returns the new block height.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

console.log(simnet.blockHeight); // 0
const newHeight = simnet.mineEmptyBlocks(10);
console.log(newHeight); // 10
console.log(simnet.blockHeight); // 10

Simnet.runSnippet()

runSnippet(snippet: string): string | ClarityValue

Run arbitrary Clarity code.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const result = simnet.runSnippet('(stx-account tx-sender)');
console.log(Cl.prettyPrint(result, 2));
// {
// locked: u0,
// unlock-height: u0,
// unlocked: u100000000000010
// }

Simnet.getContractsInterfaces()

getContractsInterfaces(): Map<string, ContractInterface>

Returns the interfaces of the project contracts. This method returns a Map of Contracts; the keys are the contract addresses. The interfaces contain information such as the available functions, data-vars and maps, NFTs, and the FTs defined in the contract. It can be used to get the list of the contracts and iterate of it.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const contractInterfaces = simnet.getContractsInterfaces();
let counterInterface = contractInterfaces.get(`${deployerAddr}.counter`);
console.log(counterInterface?.functions); // array of the functions

Simnet.getContractSource()

getContractSource(contract: string): string | undefined

Get the source code of a contract as a string.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const source = '(define-public (add (a uint) (b uint)) (ok (+ a b)))';
simnet.deployContract('contract', source, null, deployerAddr);

const contractSource = simnet.getContractSource('contract');
console.log(contractSource);
// "(define-public (add (a uint) (b uint)) (ok (+ a b)))"

Simnet.getContractAST()

getContractAST(contractId: string): ContractAST

Get the full AST of a Clarity contract.

It throws an error if it fails to get the AST or to encode it JS (which should not happen).
Note: The ContractAST TypeScript is still very simple but will be improved over time.

import { initSimnet } from '@hirosystems/clarinet-sdk';
const simnet = await initSimnet();

const counterAst = simnet.getContractAST('counter');