This repository demonstrates how to create and invoke a program on the Solana blockchain. In Solana the word program is used to describe what is often described as a smart contract in other contexts.
The Solana program provided here counts the number of times that it has been executed and stores that information on chain. It is functionally identical to Solana's hello world example.
This program differs from the Solana example code in that the client which invokes the Solana program is written in Rust. Contrary to what you might guess the most complicated part of writing a smart contract in Solana is the client.
In order to run this example program you will need to install Rust and Solana. Information about installing Rust can be found here and information about installing Solana can be found here.
Once you've completed the Solana installation run the following commands to configure you machine for local development:
solana config set --url localhost
solana-keygen new
These two commands create Solana config files in ~/.config/solana/
which solana command line tools will read in to determine what cluster
to connect to and what keypair to use.
Having done that run a local Solana validator by running:
solana-test-validator
This program must be left running in the background.
To deploy the Solana program in this repository to the Solana cluster that you have configured run:
./run.sh deploy
To run the client program you must have already deployed the Solana program. The client program sends a transaction to the Solana blockchain asking it to execute the deployed program and reports the results.
./run.sh client
Will build and execute the client program. You ought to see results that look something like this:
Connected to remote solana node running version (1.7.16).
(1418720) lamports are required for this transaction.
(499999997700801080) lamports are owned by player.
creating greeting account
(1) greetings have been sent.
On future executions you will see that the greetings counter increases.
(2) greetings have been sent.
This repository is divided into two parts. There is a program/
directory which contains the smart contract that is actually deployed
to the Solana blockchain and a client/
program which handles
collecting funds, creating accounts, and invoking the deployed
program.
Details about how the deployed program works can be found here. The actually interesting part of this repository is the client which I'll discuss below.
In order to execute a program that has been deployed to the Solana blockchain we need the following things:
The third item on that list is confusing. In Solana programs are entirely stateless. Instead, they operate on accounts and store data in those accounts between executions as needed.
Accounts are a really bad name for files. You can read the technical details here.
In order for a program to modify the contents of an account that program must own the account. This is the reason for the "transfer" part of the third item above. Our client program will create an account to store its program state in and then transfer ownership of that account to the program. The client will then later read the data in the account to see the results of the program's execution.
Why does it have to be like this? This seems like pain. The reason is that storing data on the blockchain costs money. The user of the program is expected to pay for the cost of running the program and so they must pay to create the account.
I'll now take some time to walk through the technical details of how the client collects what is needed and then submits the transaction to Solana.
The function establish_connection
in client/src/client.rs
creates
a RPC connection to Solana over which we'll do all of our
communication with the blockchain. The URL that we use for connecting
to the blockchain is read from the Solana config in
~/.config/solana/cli/config.yml
. It can be changed by running
solana config set --url <URL>
.
Our program that we will deploy tracks the number of times that a given user has said hello to it. This requires a small amount of state which we represent with the following Rust structure:
struct GreetingSchema {
counter: u32,
}
In order to make sure that this data can be serialized and unserialized independently to how Rust lays out a struct like that we use a serialization protocol called borsh. We can determine the size of this after serialization by serializing an instance of this struct with borsh and then getting its length.
To determine how much the program invocation will cost we use the
function get_balance_requirement
located in
client/src/client.rs
. The total cost of the invocation will be the
cost of submitting the invocation transaction and the cost of storing
the program state on the blockchain.
On Solana the cost of storing data on the blockchain is zero if that data is inside an account with a balance greater than the cost of two years of rent. You can read more about that here.
It appears as if the standard is to load two years of rent into
accounts so that is what we do. Source: the "programs and accounts"
section of
this
writeup. We can determine what this "two years of rent" amount is by
running connection.get_minimum_balance_for_rent_exemption(data_size)
where data_size
is the amount of data that we will be storing in
bytes.
In order to determine who will be paying for this transaction we once
again consult the solana config in
~/.config/solana/cli/config.yml
. In that file there is a
keypair_path
field and we read the keypair from where that points.
To determine the payer's balance we use our connection and run
connection.get_balance(player_pubkey)
.
If there are not enough funds in the payer's account we will need to
airdrop funds there. This is done via the request_airdrop
function
in client/src/client.rs
. Airdrops are only available on test
networks so this will not work on the mainnet.
Both the payer and the program are accounts on Solana. The only difference is that the program account is executable.
Our client program takes a single argument which is the path to the
keypair of the program that has been deployed to the blockchain. In
the get_program
function located in client/src/client.rs
the
keypair is loaded again and then it is verified that the specified
program is executable.
The function create_greeting_account
in client/src/client.rs
handles the creation of an account to store program state in. This is
the most complicated part of our client program as the address of the
account must be derived from the payer's public key, the program's
public key, and a seed phrase.
The reason that we derive the address of the storage account like this
is so that it can be located later without storing any state across
invocations of the client. Solana supports this method of account
creation with the create_account_with_seed
system instruction.
Arguments to this instruction are poorly documented and different across the Typescript and Rust SDKs. Here are what they are and their meanings in the Rust SDK:
from_pubkey
the public key of the creator of the new account. In
our case this is the payer's public key.to_pubkey
the public key that the generated account will have. In
our case this is the public key that we generate.base
the payer's public key as it is the "base" in the derivation
of the generated account's public key. The other ingredients being
the program's public key and the seed phrase.seed
the seed phrase that was used in the generation of the
generated account's public key.lamports
the number of lamports to send to the generated
account. In our case this is equal to the amount of lamports
required to live on the chain rent free.space
the size of the data that will be stored in the generated
account.owner
the owner of the generated account. In our case this is the
program's public key.You may ask yourself after reading this why we need to both provide all of the ingredients needed to generate the new accounts public key (also called its address) and the public key that we have generated with those ingredients. The Solana source code seems to suggest that this is some method for error checking but it seems slightly shitty to me.
Sending the hello transaction to the program is actually the easy
part. It is done in the say_hello
function in
client/src/client.rs
. This function just creates a new instruction
with the generated storage account as an argument and sends it to the
program that we deployed.
We can query the state of our generated account and thus determine the
output of our program using the get_account
method on our
connection. This is done in the count_greetings
function in
client/src/client.rs
.