Primer on Solana Programming
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.
Scrolling up we can see that it is, in fact, owned by the Helium subnetwork's program:
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];
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 TokenAccount
s (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.