Back to Blog

Developing a Full-Stack Project on the Stacks Blockchain with Clarity Smart Contracts and Stacks.js Part II: Backend

This is Part 2 of 3 of the Series “Developing a Full-Stack Project on the Stacks Blockchain with Clarity Smart Contracts and Stacks.js”. If you haven’t already, start at the beginning with Part 1: Intro and Project Setup.

Backend

This contract will simply post “gm” from a user to the chain for a small fee. This is done by mapping the string “gm” to the user’s unique STX address. The functionality will be basic but enough to demonstrate concepts, testing, and frontend interaction.

In gm.clar goes the code:

;; gm
;; smart contract that posts a GM to the chain for 1 STX

;; constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant PRICE u1000000)
(define-constant ERR_STX_TRANSFER (err u100))

;; data maps and vars
(define-data-var total-gms uint u0)
(define-map UserPost principal (string-ascii 2))

;; public functions
(define-read-only (get-total-gms)
  (var-get total-gms)
)

(define-read-only (get-gm (user principal))
  (map-get? UserPost user)
)

(define-public (say-gm)
  (begin
    (unwrap! (stx-transfer? PRICE tx-sender CONTRACT_OWNER) ERR_STX_TRANSFER)
    (map-set UserPost tx-sender "gm")
    (var-set total-gms (+ (var-get total-gms) u1))
    (ok "Success")
  )
)

We have data space at the top of the file and all of the functions listed after.

Data:

Functions:

There are 3 functions; 2 read-only and 1 public.

A read-only function can only perform read operations. It cannot write or make changes to the chain. As you can see, our read-only functions are simply grabbing the value of our variable and map.

Now, I’ll go line-by-line through the say-gm public function where the work is being done.

(define-public (say-gm)
  (begin
    (unwrap! (stx-transfer? PRICE tx-sender CONTRACT_OWNER) ERR_STX_TRANSFER)
    (map-set UserPost tx-sender "gm")
    (var-set total-gms (+ (var-get total-gms) u1))
    (ok "Success")
  )
)

A Clarity custom function takes the following form:

(define-public function-signature function-body)

The function definition can be any of the three Clarity function types: public, private, or read-only

The function signature is made up of the function name and any input parameters taken by the function.

The function body contains the function logic, is limited to one expression, and in the case of a public-function must return a response type of ok or err.

The function signature in this example is:

(define-public (say-gm))

The function body is:

   (begin
    (unwrap! (stx-transfer? PRICE tx-sender CONTRACT_OWNER) ERR_STX_TRANSFER)
    (map-set UserPost tx-sender "gm")
    (var-set total-gms (+ (var-get total-gms) u1))
    (ok "Success")
  )

What exactly is happening here?

Next we have:

(unwrap! (stx-transfer? PRICE tx-sender CONTRACT_OWNER) ERR_STX_TRANSFER)

There are a couple of things happening in this line so let’s break it into two:

(stx-transfer? PRICE tx-sender CONTRACT_OWNER) is using another built-in function stx-transfer? which increases the STX balance of the recipient by debiting the sender.

… But wait a minute!

(define-constant CONTRACT_OWNER tx-sender)

CONTRACT_OWNER is tx-sender.

image of animated character speaking with text "well yes, but actually no"

tx-sender returns the address of the transaction sender, but it’s context changes based on where and how it is used.

In the case of CONTRACT_OWNER, tx-sender will take the context of the standard principal that deployed the contract a.k.a the contract deployer.

Whereas within the say-gm function, tx-sender has the context of the standard principal that is calling into the function.

You can also manually change the context by using the as-contract built-in function to set tx-sender to the contract principal.

I will demonstrate this visually during manual testing.

For now, let’s get back to the expression.

The expression will return (ok true) if the transfer is successful, else it will return an (err) response. This is where unwrap! comes in. As the name suggests, it attempts to unwrap the result of the argument passed to it. Unwrapping is extracting the inner value and returning it. If the response is (ok ...) it will return the inner value otherwise it returns a throw value.

(unwrap! (stx-transfer? PRICE tx-sender CONTRACT_OWNER) ERR_STX_TRANSFER)

So the above expression is unwrapping the result of stx-transfer?

Now we have:

(map-set UserPost tx-sender "gm")

map-set sets the value of the input key to the input value. As part of this function call, I am setting the corresponding value of “gm” to the key of the standard principal calling the function.

Then:

(var-set total-gms (+ (var-get total-gms) u1))

var-set sets the value of the input variable to the expression passed as the second parameter. Here I am performing an incremental count to increase total-gms by one each time this function executes successfully.

Finally:

(ok "Success")

because we must return a response type at the end of a public function and I just want a success message on completion.

Manual Contract Calls

To make sure the contract is working you can do a few things.

Run:

clarinet check

This checks the contract’s syntax for errors. If all is well, it should return this message:

Screenshot of local development console running clarinet check. If the contract's syntax has no errors, running clarinet check should return a message that states, "Syntax of 1 contract(s) successfully checked".

Next run:

clarinet console

This loads the following tables:

Screenshot of a local development console, depicting a successful run of the Clarinet Console. This should include the clarity-repl version number, as well as two tables, described in paragraphs below the screenshot.

The first table contains the contract identifier (which is also the contract principal) and the available functions.

The second table contains 10 sets of dummy principals and balances. This data is pulled directly from the Devnet.toml file. If you open that file and compare the standard principals, you will notice they are the same.

From here I can make some contract calls to ensure the desired functionality of each function is there.

To make a contract call, we follow the format:

(contract-call? .contract-id function-name function-params)

Make the following call to the say-gm function:

(contract-call? .gm say-gm)

Did you get an err u100? There is a constant ERR_STX_TRANSFER set to err u100. Why did the function error?

It is because I just attempted to transfer STX between the same addresses.

When running clarinet console, tx-sender is automatically set to the contract deployer. You can verify this by running tx-sender from within the console. If you compare this to the data inside of Devnet.toml you’ll see that the address is the same as the one listed under [accounts.deployer]. It is also the first address in the table that loads when opening console.

This comes back to the “context” I mentioned above. You can think of that first address as the deployer and all subsequent addresses as “users” that can call into your function.

I’m going to want to change the tx-sender within console. This can be done by running

::set_tx_sender address

I can copy/paste any address from the assets table or Devnet.toml.

Now I will call get-total-gms:

(contract-call? .gm get-total-gms)
Screenshot of the console returning an error to the contract call get-total-gms. The Error will be described, "Use of unresolved contract" and give the contract identifier.

Another error? use of unresolved contract.

This is happening because I changed my tx-sender. I now have to explicitly state the contract-id as part of the contract call. This can be grabbed from the assets table. If the console has cleared and you need to bring the table up again run:

::get_contracts

Now trying the following call:

(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.gm get-total-gms)

This should return u0

(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.gm get-gm tx-sender)

This should return none

Good this is expected because I have not yet called any write functions.

Alright, let’s call say-gm.

(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.gm say-gm)

This now returns (ok "Success")

If we make calls again to get-total-gms and get-gm we should get u1 and (some "gm") respectively which means the functionality works!

You can also run:

::get_assets_maps

This is will bring up the assets table and you will be able to see the transfer of STX between addresses reflected in the balances.

The functions have now been manually tested. Before moving on, I will describe one more thing to demonstrate the context for tx-sender.

I mentioned the as-contract built-in function and how it changes the context of tx-sender from the standard principal to the contract principal.

Exit the console and replace the CONTRACT_OWNER definition with:

(define-constant CONTRACT_OWNER (as-contract tx-sender))

Now run clarinet console again and make the following contract-call:

(contract-call? .gm say-gm)

You’ll notice that it doesn’t return an error this time and you didn’t have to change the tx-sender. Why?

Running

::get_assets_maps

will provide an answer.

Notice a new address has been added to the table. A contract principal. So even though the call was made from the deployer, it was able to transfer STX because CONTRACT_OWNER was initialized as the contract and so it isn’t transferring STX between the same address, it is transferring from the deployer’s standard principal to the contract principal which is capable of holding tokens as well.

This demonstrates what the as-contract function does.

Unit Tests

Unit testing is a critical part of smart contract development. Due to the nature of smart contract and the potential use cases, smart contract developers want to ensure that they account for as many scenarios as possible.

Tests are written in typescript and a template is auto generated during project initialization.

Let’s take a look at gm_test.ts

import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.31.0/index.ts';
import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts';

Clarinet.test({
    name: "Ensure that <...>",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        let block = chain.mineBlock([
            /* 
             * Add transactions with: 
             * Tx.contractCall(...)
            */
        ]);
        assertEquals(block.receipts.length, 0);
        assertEquals(block.height, 2);

        block = chain.mineBlock([
            /* 
             * Add transactions with: 
             * Tx.contractCall(...)
            */
        ]);
        assertEquals(block.receipts.length, 0);
        assertEquals(block.height, 3);
    },
});

This is the boilerplate test code. I recommend browsing the Deno documentation provided at the top the file.

I am just going to write a single basic test as part of this example.

Clarinet.test({
    name: "A user can say gm",
    async fn(chain: Chain, accounts: Map<string, Account>) {
        const deployer = accounts.get('deployer')!.address;
        const user = accounts.get('wallet_1')!.address

        let block = chain.mineBlock([
            Tx.contractCall(
                "gm",
                "say-gm",
                [],
                user
            )
        ]);
        assertEquals(block.receipts.length, 1);
        assertEquals(block.height, 2);

        const messageMapped = chain.callReadOnlyFn(
            "gm",
            "get-gm",
            [types.principal(user)],
            user
        )
        assertEquals(messageMapped.result, types.some(types.ascii("gm")));

        const totalCountIncreased = chain.callReadOnlyFn(
            "gm",
            "get-total-gms",
            [],
            user
        )
        assertEquals(totalCountIncreased.result, types.uint(1));
    }
});

Here I’m adding a few things to the boilerplate code provided.

const deployer = accounts.get('deployer')!.address;
const user = accounts.get('wallet_1')!.address

This is grabbing the standard principals from Devnet.toml that correspond to the string we pass to accounts.get()

Tx.contractCall(
 "gm",
 "say-gm",
 [],
 user
)

Tx.contractCall is how we can simulate a contract call from a test. It takes 4 parameters:

  1. The contract name
  2. The function name
  3. Any params accepted by the function as an array
  4. The address calling the function
assertEquals(block.receipts.length, 1);
assertEquals(block.height, 2);

I need to also simulate a block being mined. Our test assumes a start at genesis block 1.

block.receipts.length accounts for the number of transactions in that block.

block.height accounts for the block height at mining.

In this case I am calling one tx and mining one block.

const messageMapped = chain.callReadOnlyFn(
 "gm",
 "get-gm",
 [types.principal(user)],
 user
)
assertEquals(messageMapped.result, types.some(types.ascii("gm")));

Here I am calling a read-only function with chain.callReadOnlyFn() which takes the same parameters as Tx.contractCall() and asserting that the result is an ascii string “gm”. Why? If the say-gm call is successful we can assume that the result of this get-gm will be that string mapped to the user.

const totalCountIncreased = chain.callReadOnlyFn(
  "gm",
  "get-total-gms",
  ],
  user
)
assertEquals(totalCountIncreased.result, types.uint(1));

Finally, I make a call to get-total-gms and assert that the total has been incremented to 1.

Now in the terminal I can run

clarinet test

and should see a successful pass:

Image of successful terminal results after running "Clarinet Test"

Clarinet offers an extensive testing suite that includes lcov code coverage reports.

Spinning up DevNet

The backend is complete. Let’s get DevNet running and hook it up to the web wallet so that it’s ready to go after building the frontend.

Make sure Docker is running.

In your terminal run:

clarinet integrate

With this command, Clarinet fetches the appropriate Docker images for the Bitcoin node, Stacks node, Stacks API node, Hyperchain node, and the Bitcoin and Stacks Explorers.

Boot up can take several minutes the first time you launch.

When complete, you should see something like this:

A terminal image after running the clarinet integrate command. Clarinet fetches and displays the appropriate Docker images for the Bitcoin node, Stacks node, Stacks API node, Hyperchain node, and the Bitcoin and Stacks Explorer

With that you’ve got DevNet running. You can read more about the interface in the docs.

There is some configuration left to do before you can interact with your frontend app.

You need to import information from your Hiro web wallet into your Devnet.toml file.

Note: Devnet.toml is not listed in .gitignore meaning the information you add to the configuration may be visible, so you want to either add the file to .gitignore, or create a separate wallet for testing if you plan to push your code to GitHub.

From your browser open up your web wallet and change your network to Devnet. This setting can be found by clicking the three dots on the top right corner and selecting Change Network.

A screenshot of the Network Settings menu of the web wallet,  with Devnet Local host selected. Two other options include Mainnet stacks-node-api.stacks.co and Testnet stacks-node-api.testnet.stacks.co

Devnet will only be available for selection while you have your local DevNet running.

Once you change networks, open up the menu again and click View Secret Key. You need to copy this and paste it in Devnet.toml under [accounts.wallet_1] where it says mnemonic=

You will be replacing the generated keys with the ones copied from your web wallet.

Configuration is now complete.

Conclusion

Thanks for reading Part II of this series! Move on to Part III: Frontend.

We have more blog posts if you're interested.
How can a Subdomain Boost my Business?
Interact with Stacks blockchain on Testnet
Developing a Full-Stack Project on the Stacks Blockchain with Clarity Smart Contracts and Stacks.js Part III: Frontend