Create a Solana dApp from scratch

Sending new tweets from the frontend

Episode 10
1 month ago
8 min read

We are so close to having a finished decentralised application (dApp) we can share with the world! Everything is ready except that we can’t send tweets from our frontend. Not so handy for a Twitter-like application.

So let’s implement this right here right now and complete our dApp! 💪

Sending tweets

Since the last episode, you might have tried to connect your wallet and send a tweet to see what it does. Well, nothing is what it does. Not only does our sendTweet API endpoint returns mock data, but that mock data will now throw an error when trying to display the tweet since we've updated the TweetCard.vue component in the previous episode.

So let’s start by removing the last bit of mock data from our frontend and implement the real logic to send a tweet to our program.

Open the send-tweet.js file inside the api folder and replace all of its content with the following code.

import { web3 } from '@project-serum/anchor'
import { Tweet } from '@/models'

// 1. Define the sendTweet endpoint.
export const sendTweet = async ({ wallet, program }, topic, content) => {
  	// 2. Generate a new Keypair for our new tweet account.
    const tweet = web3.Keypair.generate()

    // 3. Send a "SendTweet" instruction with the right data and the right accounts.
    await program.value.rpc.sendTweet(topic, content, {
        accounts: {
            author: wallet.value.publicKey,
            tweet: tweet.publicKey,
            systemProgram: web3.SystemProgram.programId,
        signers: [tweet]

    // 4. Fetch the newly created account from the blockchain.
    const tweetAccount = await program.value.account.tweet.fetch(tweet.publicKey)
    // 5. Wrap the fetched account in a Tweet model so our frontend can display it.
    return new Tweet(tweet.publicKey, tweetAccount)

Okay, we’ve got some explaining to do.

  1. First of all, we defined the signature of our sendTweet method. As with the other API endpoints, the sendTweet method accepts the entire workspace as a first parameter. In this case, we will need access to the program but also the connected wallet which we destructure from that first parameter. We also need access to the topic and the content of the tweet which we require as the next two parameters.
  2. Next, we generate a new Keypair for our brand new Tweet account. The tweet will be initialised at this Keypair’s public address and we will need the entire Keypair object to sign the transaction to prove we own this address.
  3. We now have everything we need to send a SendTweet instruction to our Solana program. Much like we did in our tests, we pass the data, the account and the signers to the sendTweet method of our program’s API. This time, we use the connected wallet as the author account.
  4. Now that the instruction has been sent we should have a Tweet account at the provided tweet.publicKey address. Thus, we use it to access the data of our newly created tweet. We do this so we can return the created tweet account to whoever is calling this API endpoint. This enables our components to automatically add it to the list of tweets to display without having to re-fetch all the tweets on the page.
  5. Finally, we wrap the fetched account data and the generated public key in a Tweet object so that our frontend has everything it needs to display it.

Adjusting the tweet form

Since we’ve made some modifications to the signature of our sendTweet API endpoint, let’s adjust the TweetForm.vue component which is the only component to use it.

The only adjustment we need to make is to ensure the workspace is given as a first parameter. So let’s start by importing useWorkspace from the composables.

import { computed, ref, toRefs } from 'vue'
import { useAutoresizeTextarea, useCountCharacterLimit, useSlug, useWorkspace } from '@/composables'
import { sendTweet } from '@/api'
import { useWallet } from '@solana/wallet-adapter-vue'

Next, let’s scroll down a bit and use the useWorkspace composable to access the workspace and feed it to the sendTweet method.

const emit = defineEmits(['added'])
const workspace = useWorkspace()
const send = async () => {
    if (! canTweet.value) return
    const tweet = await sendTweet(workspace, effectiveTopic.value, content.value)
    emit('added', tweet)
    topic.value = ''
    content.value = ''

Notice how we accessed the workspace object outside the send asynchronous method. This is because the useWorkspace composable relies on the provide/inject API from VueJS which expects to be used within the initialisation of a component. If we tried to access the workspace inside the send method, we would get an error from VueJS upon pressing the "Tweet" button.

The right commitment

Awesome! So with our sendTweet API endpoint properly wired, we should be able to send our first tweet through the frontend, right? Sadly no.

There’s a little bug in our useWorkspace composable which causes our code to throw the following error.

TypeError: Cannot read properties of undefined (reading 'preflightCommitment')

The bug is that we only provided two parameters when instantiating our Provider object when we should have given three.

const provider = computed(() => new Provider(connection, wallet.value)) // <- Missing 3rd parameter.

The missing third parameter is a configuration object that’s used to define the commitment of our transactions.

To fix this, we could simply give an empty array — i.e. {} — as a third argument which would fallback to the default configurations.

However, I’d like us to take that opportunity to understand what configurations are needed and how we can provide them explicitly.

This configuration object accepts two properties: commitment and preflightCommitment. Both of them define the level of commitment we expect from the blockchain when sending a transaction. The only difference between the two is that preflightCommitment will be used when simulating a transaction whereas commitment will be used when sending the transaction for real.

If you’re wondering why we would need to simulate a transaction, a good example would be for your wallet to show you the amount of money that is expected to be gained or lost from a transaction before approving it.

Now, what exactly is a “commitment”? According to the Solana documentation, a commitment describes how finalized a block is at the point of sending the transaction. When we send a transaction to the blockchain it is added to a block which will need to be “finalized” before officially becoming a part of the blockchain’s data. Before a block is “finalized”, it has to be “confirmed” by a voting system made on the cluster. Before a block is “confirmed”, there is a possibility that the block will be skipped by the cluster.

Therefore, there are 3 commitment levels that match exactly the scenarios describe above. They are, in descending order of commitment:

  • finalized. This means, we can be sure that the block will not be skipped and, therefore, the transaction will not be rolled back.
  • confirmed. This means the cluster has confirmed through a vote that the transaction’s block is valid. Whilst this is a strong indication the transaction will not roll back, it is still not a guarantee.
  • processed. This means, the transaction has been processed and added to a block and we don't need any guarantees on what will happen to that block.

So which commitment level should we choose for our little Twitter-like application? Looking at the Solana documentation, they recommend the following.

When querying the ledger state, it's recommended to use lower levels of commitment to report progress and higher levels to ensure the state will not be rolled back.

In our case, I wouldn’t consider a tweet being rolled back to be a critical issue. On top of that, it is very unlikely that a block containing our transaction will end up being skipped by the cluster. Therefore, the processed commitment level is good enough for our application. We’ll use it for both simulated and real transactions.

Note that using the finalized commitment level might be more appropriate for (non-simulated) financial transactions with critical consequences.

Now that we know which commitment levels to use, let’s explicitly configure them in our useWorkspace.js composable. First, we defined two variables preflightCommitment and commitment for simulated and real transactions respectively.

// ...

const preflightCommitment = 'processed'
const commitment = 'processed'
const programID = new PublicKey(idl.metadata.address)
const workspaceSymbol = Symbol()

Then, we pass these commitment levels to the Provider constructor as a configuration object. We also give the commitment variable as the second parameter of our Connection object so it can use it as a fallback commitment level when it is not directly provided on the transaction.

export const initWorkspace = () => {
    const wallet = useAnchorWallet()
    const connection = new Connection('', commitment)
    const provider = computed(() => new Provider(connection, wallet.value, { preflightCommitment, commitment }))
    const program = computed(() => new Program(idl, programID, provider.value))

    // ...

Airdropping some SOL

Alright, now surely we can send a tweet via our frontend?! Almost… 😅

If we try to send a tweet right now, we’ll get the following error in the console.

Attempt to debit an account but found no record of a prior credit.

Okay, we’ve seen this error in the past. That means, we’ve got no money. Remember how I told you that starting a local ledger always gives 500 million SOL to your local machine’s wallet for running tests? Well, the issue here is that we’re not using that wallet in our browser. Instead, we’re using our real wallet in the local cluster.

That means we need to explicitly airdrop ourselves some money before we can send transactions.

To do that, we first need to know the public key of our real wallet. We can use the dropdown menu of the wallet button for that purpose. Once your wallet is connected, click on the wallet button on the sidebar and select "Copy address".

Screenshot of our app with the dropdown menu of the wallet button opened. The first dropdown menu item called &quot;Copy address&quot; is highlighted.

Next, we can use the solana airdrop command followed by how many SOL we want to airdrop and the address of the account to credit.

We don’t need much money but let’s give ourselves 1000 SOL — why not, we deserve it. Then, paste your public key and run that command. That’s what it looks like for me.

solana airdrop 1000 B1AfN7AgpMyctfFbjmvRAvE1yziZFDb9XCwydBjJwtRN

# Outputs:
# Requesting airdrop of 1000 SOL
# Signature: 4rBNKMyRTcddaT9QHtYSD62Juk5F4AdgryiuE4N83Yj8JJVTomAnHWL8xvPitJdtDdLorSf81rBsYz89r7dXis6y4
# 1000 SOL

Alright, now we can finally send our first tweet from the frontend!

Enter some content and, optionally, a topic before hitting the “Tweet” button. You should get a pop-up window from your wallet provider asking you to approve the transaction.

Depending on your wallet provider, you should also see an estimation of how much SOL this transaction will credit or debit from your account. However, using Phantom, this often doesn’t work for me when using the local cluster as the simulated transaction just loops forever.

Screenshot of the app with a tweet filled in the form. The content is &quot;Hello world&quot; and the topic is &quot;solana&quot;. There is an arrow starting at the &quot;Tweet&quot; button pointing to Phantom wallet approve window asking the user to approve or reject this transaction.

Nevertheless, clicking on “Approve” should run the transaction for real and display the new tweet at the top of the list! 🥳

Screenshot of the app with the previously filled tweet now showing on the list of tweets on the home page.

If you refresh the page, you can see that our new tweet has been properly persisted to our local ledger.


Hell yeah, our decentralised application is finally functional! 🔥🔥🔥

Now, all that’s left to do is deploy it to a real cluster to share it with the rest of the world. And that’s exactly what we’ll do in the next episode!

View Episode 10 on GitHub

Compare with Episode 9

← Previous episode
Fetching tweets in the frontend
Next episode →
Deploying to devnet


Author avatar
Stochastic Adventure
1 month ago

Can't wait to see the devnet deployment chapter!

BTW have you tried to deploy it to the mainnet? Wonder how much SOL would it cost.

This is hands down the best Solana anchor tutorial (better than the official tutorial). Love it!

💖 1


Sending new tweets from the frontend
Author avatar
Stochastic Adventure
1 month ago

Can't wait to see the devnet deployment chapter!

BTW have you tried to deploy it to the mainnet? Wonder how much SOL would it cost.

This is hands down the best Solana anchor tutorial (better than the official tutorial). Love it!

💖 1
Author avatar
Loris Leiva
1 month ago

Thanks! I'm working on it at the moment haha. I'll make sure to mention the cost of deploying. Spoiler alert it's around 2.6 SOL for the size of that program.

💖 0
Author avatar
Stochastic Adventure
1 month ago

Thanks. I checked some of the program data accounts and saw the cost is around 2.62 SOL. I'm curious why the program data accounts have to be "max length" (around 367k bytes).

💖 0

Would you like to chime in?

You must be a member to add a reply to a discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member

Would you like to chime in?

You must be a member to start a new discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member