I participated in Building out Loud Solana hackathon. It turned out to be a good learning experience. It is my first time working on a blockchain project along with Rust and ReactJS framework and it took me some time to understand all of it.
Let’s start with the final product. Here is the Github link for the project. It is a Simple ride-sharing Dapp built in Rust and ReactJS. The code is not optimized by any means. There are a lot of bugs and many boundary cases are not handled. But it does the basic things, stores user’s data in Arweave, stores users’ and drivers’ data in blockchain along with the ride details, and transfers tokens to drivers’ wallets. Below is what I understood about storing data in Solana.
Program and Account
Solana programs are stateless as they cannot store any data. So to store data we create another account with the wallet account’s public key using program Id and a seed. This account has this program as its owner which gives the program the permission to update the account
When a user creates a wallet in Solana, he will get a unique wallet id with the public key. User can then create another account (which will store data) with a unique seed, wallet public key, and program id. This will return a PublicKey for the account created.
Steps to create account:
- User logs in with Solana wallet. Gets a wallet public key
- Program has to store some data for this user.
- To do that, we will create a Solana account using wallet’s public key, program id and a seed string.
- Program id is the id which we receive after deploying program on the blockchain
- Same seed string can be used to restore the same account next time.
- Program is owner to this newly created account. It can write data to it by sending an instruction in a transaction.
Below is a mind-blowing doodle to explain what is happening.
Creating Account using Program and Seed
Below is the code snippet.
[sourcecode language="javascript"] PROFILE_SEED="SEED"; profileAccountPubKey = await PublicKey.createWithSeed( wallet.publicKey, PROFILE_SEED, programId, ); [/sourcecode]
Note: When we call getAccountInfo with this account public key, we will get the AccountInfo object. The first time it would be null as an account has to be created. For that, we will use the below command
[sourcecode language="javascript"] SystemProgram.createAccountWithSeed({ fromPubkey: wallet.publicKey, basePubkey: wallet.publicKey, seed: PROFILE_SEED, newAccountPubkey: profileAccountPubKey, lamports, space: space, programId, }); [/sourcecode]
Few points to consider
- Account created in Solana has a rental cost. We need to give enough lamports while creating to make it rent-free. Otherwise, the account gets deleted after the lamports are used up
- For now, the size of the account is fixed and has to be mentioned during creation time. It can be up to 10MB
- The account can be created as shown above in JS or in a Rust program
Solana Program Instruction
Now we need to add/update data in it. For e.g. we want to store the user’s profile details or a game state. To make any changes in account, we need to run a transaction in the blockchain. The transaction can consist of one or multiple instructions. For the Ride-sharing Dapp, we need to store many different states. For example, storing ride’s from and to location, driver selected for the ride, ride state as started/finished/payment pending, etc.
To store all this information, we can create instructions for different states and update the account respectively. So if we want to store ride details of a user with source latitude/longitude to destination latitude longitude, we can send these details in an instruction. The program will read the instruction and update the account with this data.
If the program is a bit complex, there can be multiple instructions with different types with separate data being sent in each of them. I found that it is best to have a block of string as instruction as a good way to handle transactions. Like a string array of fixed size and sending instruction type and data in this array, so the program can interpret it accordingly.
Creating the instruction in JS
For e.g. in the app, I am storing Arweave transaction id when a user/driver saves the name and phone number. Arweave stores these details in its blockchain and it returns a transaction hash. We store this hash in Solana account. Below is another awesome doodle to explain what I meant
In the rust program, we can read the first few bytes of the array as an instruction type. Based on the type we can parse the rest of the instruction. Below is a code snippet on creating an instruction
[sourcecode language="javascript"] //accountKey is public key of account created using program id, wallet public key and seed //programid is the id of the program generated when deployed in blockchain //Instruction object, in our case, is a string array //InstructionSchema is defined to tell borsh how to deserialize the instruction object. It should match with instruction object in rust program, so it can be deserilized in program. const instruction = new TransactionInstruction({ keys:[{pubkey:accountKey,isSigner:false,isWritable:true}], programId, data:Buffer.from(borsh.serialize(InstructionSchema,InstructionObject)), }); [/sourcecode]
Solana Transaction
We have created an instruction. Now it is time to create and send a transaction. We can have multiple instructions in a transaction.
Creating Solana Transaction :- Step 1
Create a transaction object. Add instruction to it. Set fee payer account. Store recent block hash in it
[sourcecode language="javascript"] //we can add multiple instructions if we wish to transaction.add(instruction); //set fee payer transaction.feePayer=wallet!.publicKey!; let hash=await connection.getRecentBlockhash(); transaction.recentBlockhash=hash.blockhash; [/sourcecode]
Creating Solana Transaction :- Step 2
Sign the transaction
[sourcecode language="javascript"] //wallet: WalletAdapter (in our case, we used sollet.io) let signedTrans=await wallet.signTransaction(transaction); [/sourcecode]
Creating Solana Transaction :- Step 3
Send the transaction
[sourcecode language="javascript"] let signature = await connection.sendRawTransaction( signedTrans.serialize(), { skipPreflight: false, } ); [/sourcecode]
Note: While coding for the hackathon, I added 4 instructions in a single transaction I get an error that the transaction limit of 1200 something bytes was exceeded. I did not have time to look into the details, so I created two transactions for it.
Next Steps:
While working on the hackathon, storing data in blockchain took a lot of time for me to understand with many errors which I had to debug along the way.
Most of the errors which happen are due to Account/Instruction serialization and deserialization. We have to take great care that the object created in Javascript (ReactJS/VueJS) matches with the object struct in Rust.
In the next post, I will cover the other part of the application. It consists of mostly storing different states in blockchain starting from ride creation to ride completion.
Also If I get time, I will clean up and improve the code.
1 thought on “Solana Ride Sharing Application – Part I”