Solana DApp Development (Part III)- Client Interaction

Kerala Blockchain Academy
7 min readApr 11, 2024

--

By Lekshmi P G, R & D Engineer, Kerala Blockchain Academy

Prefer watching videos over reading text? Check out our YouTube video on Solana DApp Development- Client Interaction here.

Whassup? Explored the beauty of Solana Beach? What next? We need to tell someone about it. So that’s what we are going to do next. We will build a client part so anyone can interact with our application on the Solana ecosystem.

By now, we wrote a smart contract and deployed it using Solana CLI and Anchor framework. Smart contracts deployed on blockchain will get a userbase only by implementing an interaction part. So here we are going to do that. Inorder to proceed we need the Anchor project, created before. If you haven’t done that, go through this article and don’t forget to come back. There we prepared a project called hello_anchor, where we wrote a hello_world contract and deployed it to devnet.

Smart Contract Deployment

First thing we have to do is to open the hello_anchor project on VS Code. So, you can see lib.rs inside src inside the folder programs/hello_anchor. lib.rs contains the hello_world smart contract. Now we are going to deploy it in a locally simulated version of Solana because it gives an infinite amount of SOL to play with.

Inorder to start the simulation, you can use the command,

solana-test-validator --reset

This command will start a fresh instance of the Solana test validator, and upon creating slots, our default wallet will get funded with SOL.

Next, we have to rewrite the contract in lib.rs to anchor-compatible code, as this one is a raw Solana contract. So we have to replace the contract with this,

use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");#[program]
mod hello_world {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
msg!("Hello. World");
Ok(())
}
}
#[derive(Accounts)]
pub struct Hello {}

So this is our Anchor smart contract.

In this contract, we first import the Anchor dependencies

use anchor_lang::prelude::*;

Next, we need to put the ProgramId(Contract Address); as of now we don’t know that, so just putting a placeholder.

declare_id!("11111111111111111111111111111111");

Next we wrote the hello_world module, into which we import all the parent dependencies. After that, create a hello function, which logs out Hello. World message.

mod hello_world {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
msg!("Hello. World");
Ok(())
}
}

Finally, create a struct to hold the contract state.

#[derive(Accounts)]
pub struct Hello {}

We also need to make changes on Anchor.toml file so that we can connect to Solana Localnet(The hello_anchor has to be filled with a place-holder, which will be replaced later).

[toolchain]
[features]
seeds = false
skip-lint = false
[programs.localnet]
hello_anchor = "11111111111111111111111111111111"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "Localnet"
wallet = "/home/kba/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

Next thing we have to do is to get the ProgramId before deploying smart contract to the network. This can be done using a single command,

solana address -k target/deploy/hello_anchor-keypair.json

This will auto-generate an address, which is to be filled on declare_id field on lib.rs & [programs.localnet] field on Anchor.toml.

lib.rs
Anchor.toml

Next, we need to build the contract using the command

anchor build

Finally, we need to deploy the smart contract.

anchor deploy

And you can see Program Id will be the same as we auto-generated before.

Client Interaction

Now, we are going to develop our client to interact with the smart contract. We will create our RestAPI for interaction. For that, go to the app folder and initialize the node project using the command,

npm init

Just go with the defaults.

Now we need to install the dependencies, anchor & web3.js for blockchain communication and fastify for building API.

npm i @project-serum/anchor @solana/web3.js fastify

After installing the necessary dependencies, create a file main.js. Configure the project to ES6 by adding “type”:module and also write the start-up script for main.js by adding “start”: “node main.js” on package.json.

package.json

Now we have to import the dependencies to main.js

import * as anchor from '@project-serum/anchor';
import * as web3 from '@solana/web3.js';
import * as fs from 'fs';
import Fastify from 'fastify';

We use Fastify with logging for building API.

const fastify = Fastify({
logger: true
})

Then, we will load the account we created using Solana CLI.

const keypairFile = '/home/kba/.config/solana/id.json';
const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile)));
const keypair = anchor.web3.Keypair.fromSecretKey(secretKey);
const wallet = new anchor.Wallet(keypair);

Now, using the account, we try to establish a connection with Solana Localnet.

const connection = new web3.Connection('http://127.0.0.1:8899');
const provider = new anchor.AnchorProvider(connection,wallet,{
commitment:'processed'
});

Now we will get the idl, which can be found in the target folder, generated during the building of smart contract.

const idl = JSON.parse(fs.readFileSync("../target/idl/hello_world.json",'utf-8'));

Also, we need the programId.

const programId = new anchor.web3.PublicKey('3U2FfqSSoHDicxZzMLv2VKaoa3Qqg71ZLQ3Qgvtb68zW');

With all these things, we will create a program instance, which will allow as to interact with the deployed smart contract .

const program = new anchor.Program(idl,programId,provider);

Now we are going to create a route, for invoking a transaction for calling hello function.

fastify.post('/',async function handler(request,reply){
try{
const tx = await program.methods.hello().rpc();
reply.code(200).send({transactionHash: tx});
}
catch(err){
reply.code(500).send({error:"Error calling contract",details:err.toString()});
}
})

In this route, we are calling the hello() function using the RPC and send back the transaction hash as response. Here, we are handling errors using a try & catch.

Next, we are going to write another route to get the transaction details of a particular transaction hash. For this, we will be directly using the Solana RPC API method named fetchTransaction. Here will get the Transaction details as the output.

fastify.post('/fetchTransaction',async (request,reply)=>{
try{
const txHash = request.body.transactionHash;
        const response = await fetch('http://127.0.0.1:8899',{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
jsonrpc:"2.0",
id:1,
method: "getTransaction",
params: [txHash,"json"]
})
});
const rdata = await response.json();
console.log(rdata);
reply.code(201).send(rdata.result);
}catch(error){
reply.code(500).send({error:"Error fetching transaction", details: error.toString()});
}
});

Finally, we run the server.

const start = async ()=> {
try{
await fastify.listen({port:3000, host:'0.0.0.0'});
} catch(err){
fastify.log.error(err);
process.exit(1);
}
};
start();

The entire main.js will look like this.

import * as anchor from '@project-serum/anchor';
import * as web3 from '@solana/web3.js';
import * as fs from 'fs';
import Fastify from 'fastify';
const fastify = Fastify({
logger: true
})
const keypairFile = '/home/kba/.config/solana/id.json';
const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile)));
const keypair = anchor.web3.Keypair.fromSecretKey(secretKey);
const wallet = new anchor.Wallet(keypair);
const connection = new web3.Connection('http://127.0.0.1:8899');
const provider = new anchor.AnchorProvider(connection,wallet,{
commitment:'processed'
});
const idl = JSON.parse(fs.readFileSync("../target/idl/hello_world.json",'utf-8'));
const programId = new anchor.web3.PublicKey('3U2FfqSSoHDicxZzMLv2VKaoa3Qqg71ZLQ3Qgvtb68zW');
const program = new anchor.Program(idl,programId,provider);fastify.post('/',async function handler(request,reply){
try{
const tx = await program.methods.hello().rpc();
reply.code(200).send({transactionHash: tx});
}
catch(err){
reply.code(500).send({error:"Error calling contract",details:err.toString()});
}
})
fastify.post('/fetchTransaction',async (request,reply)=>{
try{
console.log("hi");
const txHash = request.body.transactionHash;
console.log(txHash);
const response = await fetch('http://127.0.0.1:8899',{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
jsonrpc:"2.0",
id:1,
method: "getTransaction",
params: [txHash,"json"]
})
});
console.log("hi");
const rdata = await response.json();
console.log(rdata);
reply.code(201).send(rdata.result);
}catch(error){
reply.code(500).send({error:"Error fetching transaction", details: error.toString()});
}
});
const start = async ()=> {
try{
await fastify.listen({port:3000, host:'0.0.0.0'});
} catch(err){
fastify.log.error(err);
process.exit(1);
}
};
start();

Now, let’s start the server using the command,

npm start

Testing the Client

So, we build the client for interaction. Let’s test it using Postman. So take your Postman to make the first transaction by calling a POST method to the port in which RestAPI is running.

In return for this call, we should get a transactionHash.

Now, we will test the second route for getting the transaction details, by invoking a Get method to fetchTransaction route by passing the transactionHash as input.

Here, we should get the Transaction Details as the response.

Conclusion

Success! We completed writing a smart contract, deploying it to the Solana ecosystem, and finally interacting with the smart contract using the Client. So, how was it? Solana may not look like a developer-friendly ecosystem, but it has much to offer. Be with us for more updates.

--

--

Kerala Blockchain Academy

One-stop solution for quality blockchain education and research. Offers best in class blockchain certification programs in multiple blockchain domains.