Skip to main content

Primer on Solana Programming

This page has not been fully updated to represent the latest state of the Helium Network following the migration to Solana on April 18, 2023.

This guide exists to give an overview of Helium data on Solana, and some general Solana programming semantics. For a more in-depth guide, check out The Solana Cookbook

Reading Data

At its core, Solana is just a giant Key/Value store. A public key is the key, and there's some blob of data at that value. That blob of data is called an Account. You can imagine Public Keys like a primary key in a traditional database.

For example, let's consider this public key: 6r4x69WHPPDzUuMpJ7vwSUiU6e73KJqeoPyZ8nHQTmFP

If we check the Solana Explorer, under Anchor Data for this account, we can see that this is a DaoV0 on the devnet helium-sub-daos contract.

Dao in Explorer

Scrolling up we can see that it is, in fact, owned by the Helium subnetwork's program:

Program Owner

This means that only the helium-sub-daos program can make changes to this account. On other chains, a Program may be called a Smart Contract. Accounts are either owned by a program or owned by the system program. An account owned by the System Program is generally either a Wallet (holding SOL), or an account that has yet to be initialized.

On Solana, a Wallet is just an account with some amount of SOL tokens in it, owned by the system program. The owner of that Wallet controls the private key to that account, and so can sign transactions as that public key.

PDAs

Program Derived Addresses (PDAs) are home to accounts that are designed to be controlled by a specific program. With PDAs, programs can programmatically sign for certain addresses without needing a private key. PDAs serve as the foundation for Cross-Program Invocation, which allows Solana apps to be composable with one another.

A PDA is a Public Key that is derived from some set of seeds and a given program ID. Think of it as a one-way hashing to a new address. So, for example,

getProgramAddress([Buffer.from("helium", "utf8")], helium_sub_daos.programId)

will always return the same address. Better yet, the helium_sub_daos is able to sign transactions as if it held the private key to this PDA.

As you will see, the Helium programming model depends heavily on PDAs as indexes into the blockchain.

If you imagine the world of Helium Data on Solana as a Graph, PDAs let us hook into particular subgraphs.

A great example of this is the relationship between the Helium dao and the hnt mint address. The DaoV0 definition from earlier exists at address:

import { daoKey } from "@helium/helium-sub-daos-sdk"

const daoAddr = daoKey(new PublicKey("...hnt mint..."))[0];
Helium PDAs and Mints

You'll notice that if you trace the graph of Helium PDAs, they always lead back to the address of either the HNT token mint, or the subnetwork token mint.

This design is intentional. Rather than making DAOs, subnetworks, etc global, we opted to make them dependent on the specific mints being used. This allows us to set up testing scenarios in which we use a testing token alongside the real tokens. This design also allows for easier reuse of our contracts throughout the Solana community.

Aside - Tokens

To understand the Helium model on Solana, it's important to understand how tokens work on Solana.

You might imagine a Wallet is some kind of account (data struct) holding all of your tokens. This is not the case!

Instead, a Wallet is really a collection of TokenAccounts (Accounts whose data is a TokenAccount). The token account itself has a reference to its owner, which is the Wallet. Note that TokenAccount.owner is different than the Account.owner. In this case, the Program that is allowed to make changes to a token account is the Token Program. That is the Account.owner. Whereas the owning wallet of the TokenAccount is the TokenAccount.owner. You can see a token account for a testing token we created here

Every token, whether it be USDC, HNT, or MOBILE has a definition. This is called the token Mint. You can also see this key on the token account above. This includes definitions like current supply, mint authority, freeze authority, and decimals.

A common confusion on Solana is understanding Accounts vs Token Accounts, owners vs Token Account owners.

Associated Token Accounts

Associated token accounts are often the first use of PDAs you will see as a developer on Solana.

Given the above model, it is possible for a given wallet to have multiple token accounts for the same token. That is both messy and hard to index. Early in the days of Solana, a program called associated-token emerged. Associated-token says that the account for a given Wallet and mint can be derived by:

 PublicKey.findProgramAddress(
[wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
ASSOCIATED_TOKEN_PROGRAM_ID
)

Generally, token accounts are created through the associated-token program so that they can be guaranteed to be accessed at an easy-to-find address.

Fetching Data

The classic way of fetching data on Solana is to use @solana/web3.js connection object.

import { PublicKey, Connection } from "@solana/web3.js"

const connection = new Connection("...rpc url...")
const account = await connection.getAccountInfo(new PublicKey("..."))

console.log(account.data)

This will get you the binary data of any account stored on chain; however, you will need to decode the data yourself. Most programs come with their own SDKs for reading and deserializing data. For example, SPL Token (the token program), comes with @solana/spl-token

import { PublicKey } from "@solana/web3.js"
import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"

const mint = new PublicKey("...wallet...")
const wallet = new PublicKey("...wallet...")
const tokenAddress = getAssociatedTokenAddressSync(mint, wallet)
const tokenAccount = await getAccount(connection, tokenAddress)

console.log(tokenAcount.amount, tokenAccount.owner)

In the case of Helium programs, they all use a framework called Anchor, which amongst other things generate SDKs for us.

You can use the SDKs as follows:

import { init } from "@helium/helium-sub-daos-sdk";
import * as anchor from "@coral-xyz/anchor";

anchor.setProvider(anchor.AnchorProvider.local("...rpc url..."));
const provider = anchor.getProvider() as anchor.AnchorProvider;
const program = await init(provider)

const daoAccount = await program.account.daoV0.fetch(new PublicKey("...dao key..."))

Sending Transactions

Transactions are how we modify data on Solana. All Helium SDKs use the standard Anchor SDK.

That means the interface to all instructions are predictable. You should be able to look at either our IDLs, or the programs themselves, and see what accounts need to be passed to generate instructions.

The best way to understand how to take certain actions against Helium on Solana is to look at our tests.

For example, to submit a Data Credit delegation, the code may look something like this:

import { subDaoKey } from "@helium/helium-sub-daos-sdk";
import { init as initDc } from "@helium/data-credits-sdk";
import * as anchor from "@coral-xyz/anchor";

anchor.setProvider(anchor.AnchorProvider.local("...rpc url..."));
const provider = anchor.getProvider() as anchor.AnchorProvider;

const iotMint = new PublicKey("...iot mint...");
const iotSubDao = subDaoKey(iotMint)[0]

const dcProgram = await initDc(provider)

await program.methods
.delegateDataCreditsV0({
amount: toBN(10, 0),
routerKey: "router key",
})
.accounts({
subDao: iotSubDao,
})
.rpc();

RPC & RPC Nodes

An RPC is an application's gateway to the Solana cluster. RPC Nodes do not participate in consensus and instead are dedicated to data requests.

All the RPCs used in the Solana Migration follow the Solana chain and will index the NFT data that Core Developers created in Compression NFTs. This is one reason the Helium Network can mint a million or more NFTs on Solana.

You may need to run an RPC service to access public Solana clusters. Many services are free, and others are paid. The Foundation uses Helius as their RPC Provider, and it’s useful to use a PRC provider that is familiar with compression. They will all support this at some point.

You can read more about RPC & RPC Nodes here.