#9 - Walk Through Solana SDK Design

Author: @SaiyanBs, @wei_sol_, @emersonliuuu

[Updated at 2022.06.22]

TL; DR

  1. Why is SDK design important?

    • Usage - frontend

    • Performance

    • As connector between client side and programs

  2. How to design SDK under Solana architecture

  3. Compare between Bad & Good design

Overview

Get data and show it on the client side.

How to Get the Data

web 2

  1. Get data by API.

  2. Mostly data is from the same or partnership company.

web 3

  1. Get data by SDK.

  2. Mostly need to get the on-chain data or others project open sourced SDK.

Example

How to Show the Data on the Client Side

Basically it's the same no matter it's web2 or web3. Nowadays most frontend developers choose frameworks to get the job done, and for now and especially on Solana, I believe Next.js is the best option.

Show it on the client side.

For instance, using Next.js, we need to split components like puzzles, split the whole page into pieces, and each piece may contain the content (layout and data).

And how we design the components depends on

  1. layout (basically follows the design)

  2. data flow

  3. performance

  4. maintenance (low coupling, readable)

  5. how SDK or API design

Sometimes these points can against each other, so I feel like designing the components and data flow is more like an art, may need to sacrifice some points to achieve another one, so just choose a better way at the moment you did this.

For Instance, NFT staking page in Dappio

Discuss

  1. What's the same part and different part between two pics ?

    • Different data by different project.

    • The pending reward card both exist in DappieGang and others project but with different layouts and position.

    • DappieGang has one utility row and filter part, but the others don't.

  2. What's the similar funtion between two pics ?

    • Get staked info

    • Get pending reward

    • Claim pending reward

    • Stake / unstaked

  3. What do we need to consider?

Let's check the first part - Overview info

As we can see, NFTs staked in pools which are accounts, so we need to know the address to get the account data.

How do we get the pool public key in v1 ? We hardcoded all of them.

Pending Reward

Then, it's the pending reward part which also exists in DappieGang's second row.

First thing we need to know is where's the reward from? The only reason we can get the NFTU is because we deposit our prove token into farm and farming/mining, so we need to know the farm's account to get the infos we need.

But where's the prove token from? The flow is

  1. We stake our NFTs into pools get prove token.

  2. We deposit our prove tokens to get farming token and mining NFTU.

So we need to know which pool to stake first, because differnt NFTs rarity stake to different pools, and this is defined by initialization, and all the rarity info also stores on chain.

As we can see, just only the pending rewards could make tons of RPC requests. And here we only go through the funtional parts, as a frontend developer, you need to deal with the component design to make it maintainable, readable, and make sure the performance won't destroy the user experience at the same time.

Example - V2 version

Problems

  1. Hard code ID in SDK

Which means once we add new category we will need to update SDK too.

And here's only part of the DappieGang infos, so as we partnership more projects, it'll end up become a file that you don't want to involved.

  1. Too many RPC request As we can see, in the pending rewards part, we need to call a tons of RPC requests to get the data we want, and it's just the pending reward part.

  2. Maintainance Again, by the pending reward example we know, you have to call functions one by one and sometimes not very intuitive, especially from the client side.

From a frontend developer's view, we need to deal with a lot of for loop, sync/async issues, components design, state management.

From a team member's view, with a poor SDK design, we need to setup a clear workflow for different roles (frontend, SDK, program).

For example, the hardcoded pool and farm infos, which side to store all of these infos, and when to update the file after initializing a new one, how to maintain this file ..etc

Goal: Design the Most Friendly SDK or API for Frontend

  • Stateless, less parameters or arguments.

  • Call anywhere we want, no need to consider the context.

Good SDK Design

Prerequisites

  • Solana system model

  • Account model

Solana system model

Reading from solana starts with sending a http request to the RPC, and the data is sync from validator.

Writing data(sending transaction) to Solana is also first sent to RPC, the RPC will lookup the leader and pass the tx packet to it via a UDP request. The transaction will next be verified and processed.

Each instructions will be executed in the program, modifying account data. New block contained state changes will be sync across validators and voted.

Storing data in Solana

Image from https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/

Rule of Thumb

1. SDK Architecture

This is an overview of a good SDK design. It can be separated into two part, read and write.

  • The "read" part of the SDK is about fetching the data from RPC and deserialized into an object.

  • The "write" part of the SDK is to create a transaction object to interact with programs.

Utility, layouts and ids is used across reading and writing.

2. Reduce RPC request

  • Each Account data only needs to fetch once.

  • Reduce the usage of Connection.

3. Stateless design

  • SDK is for reading/writing data to Solana.

  • To read form the chain, account needs to be fetched and deserialized.

  • To write to the chain, a TX is built and send.

  • SDK only handles data decoding and tx building.

  • Data is not modified by any functions/methods.

NFT Staking Program

  • A program that give out Prove Token by locking certain collections of NFT.

  • NFT is stored separately in different Vaults.

  • Mint list is manage by rarity program.

NFT Staking Program (Implement)

Find the full code base here

Program Architecture

Setup Local Test Validator

Restart local validator, and clone a useful program for creating ATA from Mainnet. See more about the program here

Configure RPC url to localnet and the wallet to the one for deploying. Make sure the wallet have enough balance (~ 10 SOL)

Build And Deploy

Clone the code from github repo

Install dependency and make sure you have replace program keys for all files below.

  1. programs/nft-rarity/src/lib.rs

  2. programs/nft-staking/src/lib.rs

  3. ts/v1/ids.ts

  4. ts/v2/ids.ts

  5. Anchor.toml

After replacing program keys, we are ready to build and deploy our program with command below.

NOTICE

Make sure the Program Id you get after deployed match with the one in program declare_id!("6Utx...QnKM"), otherwise transaction we send might failed.

SDK v1

Before we start staking NFT to program will need to initialize the allowed mint list to rarity info in rairty program, then initialize pool info with the rarity info key we just initialized.

DEBUG

If you have some error message like something below, run yarn add ts-mocha might solve. Thanks to this post.

Import library and declare variables

After initialization, we can implement staking part in test/v1/1_nft-staking-v1.ts, let's paste the code below to the file.

Add log before and after staking

Implement stake/unstake

Now, we are good to implement staking part! Let's think about what we need for building stake transaction.

Arguments we need for staking:

  • staking pool info

    • pool info key

    • prove token mint

    • prove token authority

    • ...

  • user info

    • user address

    • NFT mint

    • prove token ATA

Pool info key is one of the argument we're going to use later, so we need to generate it first.

Add stake and unstake test.

Now, we can run command below to see all test works well or not.

Implement stake/unstake transaction in SDK

After running test, you might found we didn't actually stake our NFT to program since we didn't implememt the logic for stake and unstake in SDK yet. Let's add the logic in ts/v1/transaction.ts and run the test again.

Stake

Unstake

Run command again, then you can stake all your NFT to the program now!

SDK v2

You might notice there are some problems while we implementing stake/unstake.

  1. We need to know corresponding pool info key before we use it, and there are two way to get the key. One is generate with the seed as we done previous, and another is hard coded in SDK. Someone who used this SDK won't know the seed, so pool info key might need to hard coded in SDK and this result in frequently update SDK due to new partner joined.

  2. Since we only have pool info key, we must fetch account info when building transaction. You can imagine if user is going to stake lots of NFT in different pool that might be a disaster for just building transaction by sending tons of RPC request.

Refactor SDK

In order to solve the issues above, we will try to

  • Make stake/unstake transaction/instruction stateless

  • Remove hard coded stuff

Let's take a closer look at v1 SDK, the root cause of why we need to make multiple RPC request comes from the bad design of the interface. If we only had pool info key associated with PoolInfo, we would force fetching data in every function that requires PoolInfo data except the key. Actually, we can extract the fetching data from building transaction by implementing a fetch function, so called fetchAll(), to get all account we need for no matter matching NFT with pool or building transaction.

This is what fetchAll() function do, can see the AllInfo class definition in ts/v2/poolInfos.ts

After implementing fetchAll() with some class to store the data, now we only fetch data at the beginning by calling fetchAll(), then we can pass the data as an argument for building transaction.

Implement test with v2 (refactored) SDK

Open tests/v2/1_nft-staking-v2.ts you will see the code below (no need to do any modification). You can see that we don't need to generate pool info key first neither hard coded those keys in SDK now, we get all data with this line of code: allInfos = await nftFinanceSDK.fetchAll(provider);

Run command below to make sure no issue occured.

Implement stake/unstake transaction in SDK v2

Once all test passed, let's add the logic for stake and unstake transaction in ts/v2/transaction.ts. Since we pass in full pool info data instead of only the key, we can remove all RPC request during transaction building.

Stake

Unstake

Run command below again and now we successfully use refactored SDK to stake NFT!

Difference Between v1 and v2 SDK

  • How we get account data

    In v1 we fetch one account since we only get one pool info key at a time, but in v2 we done the fetching part at the beginning, and fetch all account with same structure in one RPC request.

  • Where we get account data

    In v1 we hard coded the account address in SDK instead of storing data in client side by fetching all account we need at a time. In v2 we implement with the opposite way, which reduce the frequency of fetching same account in different function.

  • Maintainability

    In v1 we'll need to update the hard coded stuff if new partner joined or we add new category which is tough to maintain. In v2, by replacing hard coded stuff with storing class in client side, there's no need to modify SDK due to new pool been created.

Reference

Last updated