We are going to make a CrowdFunding platform like GoFundMe, Kickstarter, and Indiegogo. Our DApp will let people create campaings, Donate SOL to existing campaings and payout to the compaign creator. We are going to make a Solana program and connect it with our front-end application.
The following software is required to complete this tutorial:
- Git, install it from HERE.
- Solana CLI, install it from HERE.
- The Rust toolchain, install it from HERE.
- Node.js (v14.18.1+), install it from HERE.
Rust is a multi-paradigm, high-level, general-purpose programming language designed for performance and safety, especially safe concurrency.
Rust code uses snake case as the conventional style for function and variable names.
You can follow this this link for your operating system.
Before we start the tutorial, we need to understand some basics of Rust. I have added the link to the Rust book pages if you want to read more about any topic.
Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters. Integers are
usize, and the list goes on here basically
uprefix suggests that we have an unsigned integer and the suffix number tell the number of bits. So
u8is an unsigned 8-bit number(0 to 255).
f64for floating-point numbers.
boolfor booleans, and
charfor characters. Rust has 2 types for strings,
Stringis a growable, heap-allocated data structure.
stris an immutable fixed-length string somewhere in memory.
We can create a variable with the
we can also set the data type for a variable, eg.
In Rust, all the variables are immutable by default. Which means their value can not be changed once set. And here comes the
mutkeyword. We can initialize the variable with
let mutto have a mutable variable. eg.
We can use
elsestatement in Rust just like we can do in other language, here is a small program for us to understand the syntax.
We also have loops in Rust. We can create a loop with 3 keywords
foris most common. Here is an example of it. You can checkout example for
Function definitions in Rust start with fn and have a set of parentheses after the function name. The curly brackets tell the compiler where the function body begins and ends.
For this tutorial, we can assume macros are also functions. They end with
Read more about functions on Rust book.
Rust has enums. They are more than simple enums other languages provide. In Rust, we can even store data in the enums. Here is the example of
Result enum. We are going to make use of the
Resultenum in our program.
Read more about enums on rust book.
Here is an enum and match example with an explanation of each line:
Read more about match syntax in the Rust book.
Cargo is the Rust package manager. We use it to build our program and get dependencies. It also makes adding packages(crates) very easy. Read more about Cargo in the Rust book
Borsh stands for Binary Object Representation Serializer for Hashing. It is meant to be used in security-critical projects as it prioritizes consistency, safety, speed, and comes with a strict specification. We are using it for data serialization and deserialization. Read more about the carte on crates.io.
This is all the Rust we would need to get started with our Solana program.
- We create a React app. Open your projects directory in the terminal and run
This creates a React app for us.
- Now, we will create our program.
In your projects directory.
This will create a new directory called
program, which is a new Rust project generated by cargo.
We will discuss the front-end side of the project later. Now we can open the
programfolder in VSCode.
Xargo.tomlin the program directory.
- Update your
We have added all the dependencies we are going to need for our program. Run
cargo checkto get all the dependencies. We can now start working in
src/lib.rsand start coding our program for the Solana blockchain.
Before we start writing the code, let us discuss what entry points our crowdfunding app should have.
- Create a Crowdfunding Campaign.
We would need an entry point that anyone can use to create a crowdfunding campaign on our platform. We can have a name, description, and admin fields for it.
- Withdraw from the Campaign.
We would need an entry point for campaign administrators only, so they can withdraw funds.
- Donate to a Campaign.
We would need an entry point that can be invoked by anyone to donate to a specific Campaign.
These are all the entry points we are going to need for our project. Let us discuss how we will be creating these.
Before we start we have to understand Solana programs don't have storage(
contract storageyou might be familiar with). Then how and where should we store our data.
Program accounts In Solana, data can only be stored in accounts. So we can create program-owned accounts to save the data.
One way to deal with this is to create an Account with very large storage. However if we do that, the maximum data limit for an account is 10 megabytes. If we have enough users, we would eventually run out of storage space. We must think of a way to increase the amount of storage we can use.
We can create as many program-owned accounts as we want, so the idea here is that we will have a size limit for every element in our map. And whenever we want to add a new element we will create a new program-owned account. Program-owned accounts are also called PDA(program-derived accounts).
Now that we have discussed what we want to create. Let start coding. Go ahead and open up the
programfolder in VSCode or your favorite IDE. File structure in the
programdirectory should look like this.
Go ahead and open up the
lib.rsfile in your code editor, and let us add some boilerplate code first.
You can see the completed code on github.
Here the line
Ok(())is equivalent to
In the code, I have mentioned there is only one entry point in the Solana program. But we want three as we discussed in the "What do we want in our program?" section. Let's fix this issue. Have you noticed there is no limit to the instruction_data array? We are going to take advantage of that fact. We use the first element of the array to know what entry point we want to call. Notice we can have 256 entry points like this in a single program (
u8has a value of 0..255). Realistically we never do that if in case we want that many entry points for a project. It is better to deploy more programs.
Okay, let's do more coding...
We will create a struct in Rust. We have not discussed structs above so, I will explain them here. In Rust, we do not have class. If we want to store more than 1 variable (group variables) we create a struct.
Now you must be wondering what is the meaning of
#[derive(Debug)]. It is interesting to note that we can derive some traits for our struct.
Traits : A trait in Rust is a group of methods that are defined for a particular type.
Now let's code our
CampaignDetailsstruct. I have added the fields name, admin, description, image_link,amount_donated for our Campaign.
We need to derive both BorshSerialize and BorshDeserialize. BorshSerialize is used to convert the struct into an array of u8, which is the data we can store in Solana accounts. It is also the data we have in
instruction_dataso we can deserialize that to a struct with the help of
Code: At the top of the file import
Now let's add the code of our
create_campaignfunction and the
CampaignDetailsStruct. I have added an explanation to each line in the code.
We can use the
next_account_infofunction to get an account from the array. This function returns a result enum. We can use
?Operator on the result enum to get the value. If in case of an error the
?Operator will chain the error, and our program will return the same error which was returned by next_account_info.
Solana programs can only write data on a program-owned account. Note that writing_account is a program-owned account.
By deriving the trait
CampaignDetailsstruct we have added a method
try_from_slicewhich takes in the parameter array of u8 and creates an object of CampaignDetails with it. It gives us an enum of type results. We will use the
expectmethod on result enums to and pass in the string which we can see in case of error.
Solana accounts can have data, but size has to be specified when it is created. We need to have a minimum balance to make it rent exempt. For this project, we create an account that already has a balance equal to the minimum balance. You can read more about solana account and rent exemption here.
If all goes well, we will write the
writing_account. Here on our
input_datavariable (of type
CampaignDetails), we have a method serialize. this is because of the
BorshSerializederivation. We will use this to write the data in a
writing_account. At the end of the program, we can return
Hurry! We are done with the first
create_campaignfunction. Let's continue writing the contract and write the withdraw function next.
For the withdraw function also, we create a struct to get the input data. In this case, input data is only the amount we want to withdraw.
Now let us write the function.
For the withdraw also we will create iterator and get
writing_account(which is the program owned account) and
Now we will get the data of campaign from the
writing_account. Note that we stored this when we created the campaign with
We do not want the campaign to get deleted after a withdrawal. We want it to always have a minimum balance, So we calculate the
rent_exemptionand consider it.
We want to donate to a campaign, however we can't decrease the balance of an account not owned by our program in our program. This means we can't just transfer the balance as we did in the withdraw function. Solana policies state: "An account not assigned to the program cannot have its balance decrease."
So for this, we will create a program-owned account in our front-end and then perform the SOL token transaction.
We get 3 accounts here, first is the program-owned account containing the data of campaign we want to donate to. Then we have a
donator_program_accountwhich is also the program-owned account that only has the Lamport we would like to donate. Then we have the account of the
Here we get the
campaign_dataand we will increment the
amount_donated, as the total amount of data donated to this campaign will increase.
Then we do the actual transaction. Note that the
donator_program_accountis owned by program so it can decrease its Lamports.
Then at the end of the program we will write the new updated
writing_account's data field and return the result
Hooray, We have completed our Solana program, Now we can go ahead and deploy it.
We are going to deploy the program on Devnet.
Solana Programs work on a BPF system, so we will compile our program into a compatible format.
We can use the handy package manager cargo to do this:
We can use this command to create a build. In this command, the manifest-path should be the path of your
Cargo.tomlfile. This will output the compiled program in Shared Object format (.so) in the
Now that we have compiled our program we can deploy it.
You will need the Solana CLI installed.
We will create a new Solana account to deploy the program. Run the following command:
The command will prompt you for a passphrase to secure the recovery seed phrase:
You can choose a passphrase or leave it empty. Continuing will provide both the public key of the account and the seed phrase used to create the private key:
Once complete you will have the
keypair.jsonfile, containing the private and public key of a new Solana account. It is important to keep your keypair safe. Do not commit this file to a remote code repository. It is best to add this file to a
.gitignoreto prevent this from happening.
Now we are going to request an airdrop of SOL tokens on the Solana Devnet. This command will add 1 SOL token to the account:
If you get insufficient balance while deploying, you can re-run the command to airdrop funds on Devnet.
Now we will use the following command to deploy. Note that the path of
dist/program/program.somight be different in your case. Please check and then run the command.
Hooray! we have deployed our program. We will get the program id as output.
We can verify this by checking on the Solana Explorer for Devnet.. We can search our program id here.
Hooray! We have completed everything to do with Rust for this tutorial and successfully deployed our program. Now, let us move forward and build the React app.
We have created a React app, so we can open the
crowd-fundingdirectory in our code editor. This is not a React tutorial, so we will not go into the details of React. But I will be explaining what we are going to do.
Let's first clean our project. We will remove
app.test.js. Also remove the usage of
reportWebVitals.jsin index.js. Now the project should look like:
We will create a basic UI for the app. I have used sementic-ui to do that.
If you want the UI part only and continue on with integrating the Solana web3.js library, you can use the UI template I created for you from this GitHub branch.
@solana/web3.js Let us add the
We will also use the
@project-serum/sol-wallet-adapterpackage to connect our app with sollet wallet.
And we will also need borsh for serialization and deserialization.
Let us create a new directory named
crowd-funding/srcdirectory. We will write all the Solana related code in this folder for easy reference.
Create a new file in the
index.js, and add the following code:
Solana calls its networks clusters, if you have by any chance deployed the program on testnet/mainnet, you will need to change the cluster variable to the URL for that cluster. Also update the programId, this should be the public key you got after deploying your program.
Next, create two helper functions:
instructionswill contain all the instructions we want to perform in this transaction.
transactionobject containing the instructions and we can pass it to the
signAndSendTransactionfunction to make our transaction.
CampaignDetails. We have created a class and we will call it
CampaignDetails. For deserialization and serialization we have to create a schema for our class. We will create map, and match the types of each field. Note that
PubKeytype is nothing but a u8 array with length 32.
Now we can write our
createCampaignfunction. This function takes
image_linkas input parameters.
The first thing we will do it to add a call to
We will check if the wallet is connected or not, and connect if it isn't.
We will create a pubkey for our program account which will contain the data of a campaign, this is
writing_accountwe have used in our program.
Now we have created a publicKey. Note that we have not given an instruction to create an account yet.
Let us setup the campaignDetails we want to send to our program.
Convert the data to
Uint8Array. Note that all the programs have the
instruction_datadatatype, an array of u8. And before we send this data remember we want the first element in our instruction_data to be (0,1,2) for calling different entry points. We will set it to 0. As we want to call
Now we have the data we want to send to our program.
We will fund it with the minimum number of lamports required to make it rent Exempt. So we calculate the lamports required like this.
Here we create the instruction to create our account we will pass in the pub key, the size of data it needs to store, initial lamports, and other parameters. This is the first instruction we will create.
Now we can create the 2nd instruction. We invoke our program that would write the data to the program account we just created in the above instruction.
In the keys parameter we will pass all accounts we want to send and in data (
instruction_datafor our program) we want to send, we will pass the programId we want to invoke. Note we have created programId global variable in this file already.
Now we have created the instructions we want. We will pass them to our helper function
setPayerAndBlockhashTransactionwhich would create a instance
instructionswhich we can then pass to
Now that is done we can confirm our transaction by passing the
Now we can go ahead and connect this function to our front-end with our Form.
So in our
form.js, we can have an on submit. On calling this function we will see a sollet dialog. Which would ask us to sign the transaction.
See the final
form.jsfile for reference. Now we can add campaigns.
We have implemented a function to add campaigns, but we do not have any function to fetch all the campaigns. Let's make that.
We know we have added all the data in program accounts, so we can fetch all the program accounts by using
getProgramAccountson connection. This will return us a list of accounts and their public keys. Now we can deserialize the data of an account by using the borsh package and convert the data into a desirable list and return it.
Then we can use this in
app.js, we can render cards with this data. Add this code to
See the final
app.jsfile for reference.
For donating, we will again create an program account and we will fund it with the amount we want to donate. As we discussed while writing our program it is not possible to decrease the balance of an account that is not owned by our program.
Our function will take in the
amount: the amount of Solana token we want to donate.
campaignPubKey: public key of the account we want to send tokens to. This is the same account where the data related to this campaign is stored.
This is similar to
createCampaignfunction. Here I have set the space as
1, because I want the account to get deleted when its balance becomes zero. And we are setting the initial lamports equal to the number of Solana tokens we want to donate.
Then we will pass 3 keys (Accounts) as our program needs 3 accounts (see
donatefunction implementation in the program).
We are sending data as an array, , because we want call the donate function in the program and we have mapped it to the 2 value of the first element in the
We have created instructions to our program and then called the
confirmTransactionto send and confirm the transaction just like we did in the
We can connect this function with the UI. In
card.js, update the
See the final
card.jsfile for reference.
Now we will write the withdraw function. For withdrawls, we don't have to create a program account, and we will only pass one instruction. Since we are using a
WithdrawRequeststruct in our program, we will have to create a class and schema for borsh serialization. Let's set that up now:
We have created the schema with the amount as
u64which is the datatype of the variable in Rust. Now let us write the actual function. Our function will take in the parameter
amountwe want to withdraw. Then we will serialize the data. First, we will create the
WithdrawRequestobject, and then with the help of the schema, we have as a static member in
Then we will create the instruction and pass the data. Note that we are inserting
This part of the code is same as the
Connect the functions with the UI in
See the final
And we're done!
You can see the whole project on github.
In this tutorial, you learned how to build a Crowd Funding app on Solana. We covered the on-chain program's code using the Rust programming language. We built the User Interface with React.
This tutorial was created by Sushant Chandla.