Solana Ride Sharing Application – Part 2

In the previous post, we covered the basics of how to store the data in the Solana blockchain. In this post, we will look into storing details in the blockchain based on the current state of the application.

Application States

Eventually, the code has 10 states/events. In each state, we add or update data to the account. I will go through them briefly. I will post some snippets of code here. The full code of the application is available here.

User/Driver Profile

After a user connects the wallet, the applications give a form to store the name and phone number. I want it to be extensible so I am storing data in Arweave. Arweave is a decentralized storage blockchain. Initially, I was not sure if I would go with the ride-sharing app option or an NFT app, so using Arweave made a lot of sense. In an NFT project, we can store NFT data in Arweave.

Setting up Arweave

I followed the below steps to get Arweave running.

  • Install docker
  • Get the Testweave docker image from here
  • Get the image up, using docker-compose up.
  • In dapp code, a simple class is setup to get arweave service reference. Below is the snippet of the class. Full source code is available in github repo.
class ArweaveService{
    arweave:Arweave;
    testWeave?:TestWeave;
    walletKey?:JWKInterface;
    
    constructor(){
        this.arweave=Arweave.init({
            host:"localhost",
            port:1984,
            protocol:"http",
        });

        TestWeave.init(this.arweave).then((testWeave)=>{
            this.testWeave=testWeave;
            this.walletKey=this.testWeave.rootJWK;
        });

    }
}

Few of the issues which I encountered while setting up Arweave

  • If you are using WSL in windows, stop the net service with this command. net stop service else there is a conflict while running the Arweave server
  • In docker-compose.yml, Postgres ports:- “5432:5432”, I changed it to “5432” else there was a port conflict.

Saving data in Arweave

Below is the source code to save data in Arweave. It is similar to updating data in Solana.

private async saveData(jsonData : string):Promise<string>{
        let transaction = await this.arweave.createTransaction({
            data:jsonData
        },this.walletKey!);
        //Buffer.from(jsonData,'utf-8')

        transaction.addTag("Content-Type","text/plain");

        //sign
        await this.arweave.transactions.sign(transaction,this.walletKey!);
        //post
        await this.arweave.transactions.post(transaction);

        await this.testWeave!.mine();

        const hash=transaction.id;
        const status=await this.arweave.transactions.getStatus(hash);

        return hash;
    }

In the above code, we are signing and then posting the transaction to Arweave. Then we wait for the transaction to mine and get the hash. We store this hash in Solana Account.

Storing Arweave Hash in Solana

The above code gives us the hash of the account JSON data stored in Arweave. Now we store this hash in Solana. In the previous post, I have mentioned how to create an instruction, transaction, and publishing it on the blockchain. Now we sell all of it together to store Arweave data. We will use a similar code for all the different states which we need to store to get the application running.

Disclaimer:- The below code is not best optimized or follows the best practice. It just shows how we can store data in the blockchain using a string array. There is a lot of scope for improvement.

[sourcode language="javascript"]

    const instr: SingleInstruction = new SingleInstruction();
             
    let instr_type="1";
    if (accountType===AccountTypes.Driver)
         instr_type="3";
    instr.full_instruction=replaceAt(instr.full_instruction,0,instr_type);
    instr.full_instruction=replaceAt(instr.full_instruction,2,hash);


   async saveSingleAccountDataInstruction(
        connection:Connection,
        pubKeyString: string,
        instrRecord: SingleInstruction,
        wallet:WalletAdapter
    ):Promise<RpcResponseAndContext<SignatureResult>>{
        const profileKey=new PublicKey(pubKeyString);
        
        
        const profileInstruction = new TransactionInstruction({
            keys:[{pubkey:profileKey,isSigner:false,isWritable:true}],
            programId,
            data:Buffer.from(borsh.serialize(SingleInstructionSchema,instrRecord)),
        });

        const trans=await setPayerAndBlockhashTransaction(
            wallet,
            profileInstruction
        );

        const signature = await signAndSendTransaction(wallet,trans);

        const result= await connection.confirmTransaction(signature,"singleGossip");

        console.log(result);
        return result;

    }
[/sourcecode]

Application States To Save

The above code is saving the transaction to the blockchain. Now we will go through the different events which happen in the application. For each event, we will store the state in the blockchain. Each will have a separate instruction type so that the rust program can parse the data in the instruction accordingly.

  • Lets start with step 1, when the user stores profile data.
//At 0 position we store the instruction type. 1 is store customer data and 3 to store driver data
instr.full_instruction=replaceAt(instr.full_instruction,0,instr_type);
//From position 2 to 46 we store Arweave transaction hash
instr.full_instruction=replaceAt(instr.full_instruction,2,hash);
  • Then in step 2, the customer stores ride data i.e. source and destination latitude/longitudes and distance in km(s) from source to destination.
//Instruction type 2
instr.full_instruction=replaceAt(instr.full_instruction,0,"2");
//store ride details for customer
//at position 45 we store source localtion latitude
instr.full_instruction=replaceAt(instr.full_instruction,45,pfromLat);
//at position 53 we store source clocation longitude
instr.full_instruction=replaceAt(instr.full_instruction,53,ptoLat);
//at position 61 we store destination location latitude
instr.full_instruction=replaceAt(instr.full_instruction,61,pfromLong);
//at position 69 we store destination location longitude
instr.full_instruction=replaceAt(instr.full_instruction,69,ptoLong);
//at position 77 we store distance in km(s) from source to destination
instr.full_instruction=replaceAt(instr.full_instruction,77,pdistance);
  • Following instruction is for step 4 when the driver selects the customers and this information gets updated in the customer’s account. So the customer can see matched drivers. The current code has a limit of upto 3 customers per driver. Code is calculating min and max distance for selected 3 customers if the driver starts the ride on confirmation of at least 2 customers. So we are storing possible min and max distance value to the customer, so the customer will have a cost range if the driver starts the ride before confirmation from the third customer.
//loop through the customers selected by driver for the current ride.
instr.full_instruction=replaceAt(instr.full_instruction,0,"4");
//for min option, minimum distance is stored  for the selected customers 
instr.full_instruction = replaceAt(instr.full_instruction,77,minmax[0][1]);
instr.full_instruction = replaceAt(instr.full_instruction,186,minmax[0][0]);
//for max option, maximum distance is stored  for the selected customers 
instr.full_instruction = replaceAt(instr.full_instruction,190,minmax[1][1]);
instr.full_instruction = replaceAt(instr.full_instruction,194,minmax[1][0]);
//driver address
instr.full_instruction = replaceAt(instr.full_instruction,98,walletAddress!.toBase58());
//customer address
instr.full_instruction = replaceAt(instr.full_instruction,142,d.customers[i]);
  • The below instruction is for step 5 when the customer selects the driver. The instruction updates both customer and driver account
//instruction type 5
instr.full_instruction=replaceAt(instr.full_instruction,0,"5");
//driver
//from position 98 to 141 we store base58 driver address string in customer account
instr.full_instruction = replaceAt(instr.full_instruction,98,driverAddress);
//from position 142 to 185 we store base58 customer address string in driver account
instr.full_instruction = replaceAt(instr.full_instruction,142,walletAddress!.toBase58());
  • Then we go to step 6. As customers start confirming the ride, the driver waits for confirmations from 2 customers. Then the driver updates the customer that he is starting and the customer’s ride status updates to start.
//step 6
instr.full_instruction=replaceAt(instr.full_instruction,0,"6");
//confirmed ride string. It store 0 or 1 based on if driver confirmed the ride for customer.
instr.full_instruction = replaceAt(instr.full_instruction,190,confirmedRides[i]);
//this is the customer number i.e 1,2 or 3 serial no. of customers selected by driver. Currently this is being sent as we are updating customer data to show driver is not available to customer as he has already started the ride in case if confirmed ride bit is 0
instr.full_instruction = replaceAt(instr.full_instruction,194,""+(i+1));
//driver account address
instr.full_instruction = replaceAt(instr.full_instruction,98,walletAddress!.toBase58());
//customer account address
instr.full_instruction = replaceAt(instr.full_instruction,142,d.customers[i]);
//distance of customer rides added
instr.full_instruction = replaceAt(instr.full_instruction,186,d.ridesDistance);
//driver distance covered. This and above field are used to calculate the ride cost
instr.full_instruction = replaceAt(instr.full_instruction,77,d.distance);
//public key to pay later. Storing public key, so payment can be done later.
instr.full_instruction = replaceAt(instr.full_instruction,198,wallet!.publicKey!.toBase58());
  • The below instruction is for step 7. It just updates drivers’ account data with flat (0 or 1) for confirmed rides for selected customers.
//step 7
instr.full_instruction=replaceAt(instr.full_instruction,0,"7");
//confirmedRides is 4 bit string. each bit stores confirmed ride flag for customer selected 
instr.full_instruction = replaceAt(instr.full_instruction,190,d.confirmedRides);
  • Few steps are left now. In step 8, Driver marks the ride as finished.
//step 8
instr.full_instruction=replaceAt(instr.full_instruction,0,"8");
//ridesToFinish is string of bits to store ride completion status. All bits become 0 when all rides are finished
instr.full_instruction=replaceAt(instr.full_instruction,190,d.ridesToFinish);
  • The below instruction is for step 9. The customer pays the due amount.
//step 9
//get driver address
const driverAddress:any = getSelectedDriver(customerAccountData);
//get the amount of token
const val:number=d.cost*1*1000000000; //convert to lamports
//get driver public address to transfer the token
const pubKeyDriver:string=customerAccountData?.driver_pub_key;
const pubKeys=[walletAddress!.toBase58(),driverAddress];
const instr:SingleInstruction = new SingleInstruction();
instr.full_instruction=replaceAt(instr.full_instruction,0,"9");
instr.full_instruction = replaceAt(instr.full_instruction,142,walletAddress!.toBase58());
await profileService.sendMoneyAndUpdateDriver(connection!,wallet,pubKeyDriver,val,pubKeys,instr);
Note:- In the source code I am adding a system instruction in js code to transfer the tokens. It would be better if this code is in rust as it can be edited.
const instruction = SystemProgram.transfer({
            fromPubkey: wallet!.publicKey!,
            toPubkey: destPubkey,
            lamports, 
          });
  • There is one more step to clear the data stored in accounts

Solana UI Callback

One more useful javascript function in Solana web3 is connetion.onAccountChange. This function has two parameters, one for the Solana account and the other is a callback function. Whenever there is a change in data for the associated Solana account, this callback is triggered.

Summary

Once you get the hang of understanding how data is stored in the blockchain and how applications can interact with it, developing applications becomes easier. There is a change in data or state, we store it in the blockchain.

Tagged with: , , , , , ,

Leave a Reply