I am learning the basics of creating dApps using Solana/Anchor by following these series of articles. It is now outdated, but nonetheless, it's a great way to start.
So, straight to the question details. The contract looks like this:
use std::mem::size_of;use anchor_lang::prelude::*;declare_id!("B3W8FdURNLEEJncJNEFfu3Jfui7PTbpAcuXDnbA4Ctin");#[program]pub mod solana_twitter { use super::*; pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> Result<()> { if topic.chars().count() > 50 { return err!(ErrorCode::TopicTooLong); } if content.chars().count() > 280 { return err!(ErrorCode::ContentTooLong); } let tweet = &mut ctx.accounts.tweet; let author: &Signer = &ctx.accounts.author; let clock: Clock = Clock::get().unwrap(); tweet.author = *author.key; tweet.timestamp = clock.unix_timestamp; tweet.topic = topic; tweet.content = content; Ok(()) }}#[derive(Accounts)]pub struct SendTweet<'info> { #[account(init, payer = author, space = Tweet::LEN)] pub tweet: Account<'info, Tweet>, #[account(mut)] pub author: Signer<'info>, pub system_program: Program<'info, System>,}#[account]pub struct Tweet { pub author: Pubkey, pub timestamp: i64, pub topic: String, pub content: String,}const DISCRIMINATOR_LENGTH: usize = 8;const PUBLIC_KEY_LENGTH: usize = size_of::<Pubkey>();const TIMESTAMP_LENGTH: usize = size_of::<i64>();const STRING_LENGTH_PREFIX: usize = 4;const MAX_TOPIC_LENGTH: usize = 50 * STRING_LENGTH_PREFIX;const MAX_CONTENT_LENGTH: usize = 280 * 4;impl Tweet { const LEN: usize = DISCRIMINATOR_LENGTH // Discriminator (present in all accounts by default)+ PUBLIC_KEY_LENGTH // Author.+ TIMESTAMP_LENGTH // Timestamp.+ STRING_LENGTH_PREFIX + MAX_TOPIC_LENGTH // Topic.+ STRING_LENGTH_PREFIX + MAX_CONTENT_LENGTH; // Content.}#[error_code]pub enum ErrorCode { #[msg("The provided topic should be 50 characters long maximum.")] TopicTooLong, #[msg("The provided content should be 280 characters long maximum.")] ContentTooLong,}
In episode 10, the author introduces commitment levels - and uses them to guarantee that our transaction performed successfully. He does that by using this code:
useWorkspace.js:
import { computed } from "vue";import { useAnchorWallet } from "solana-wallets-vue";import { Connection } from "@solana/web3.js";import { AnchorProvider, Program } from "@coral-xyz/anchor";import idl from "../../../target/idl/solana_twitter.json";let workspace = null;export const useWorkspace = () => workspace;const preflightCommitment = 'confirmed'const commitment = 'confirmed'export const initWorkspace = () => { const wallet = useAnchorWallet(); const connection = new Connection("http://127.0.0.1:8899", commitment); const provider = computed(() => new AnchorProvider(connection, wallet.value, { preflightCommitment, commitment })); const program = computed(() => new Program(idl, provider.value)); workspace = { wallet, connection, provider, program, };};
send-tweet.js:
import { web3 } from "@coral-xyz/anchor";import { useWorkspace } from "@/composables";import { Tweet } from "@/models";export const sendTweet = async (topic, content) => { const { wallet, program, connection } = useWorkspace(); const tweet = web3.Keypair.generate(); const txSignature = await program.value.rpc.sendTweet(topic, content, { accounts: { author: wallet.value.publicKey, tweet: tweet.publicKey, systemProgram: web3.SystemProgram.programId, }, signers: [tweet], }); // fetch the newly created account const tweetAccount = await program.value.account.tweet.fetch(tweet.publicKey); return new Tweet(tweet.publicKey, tweetAccount);};
which is later utilized in App.vue:
<script setup>import { useRoute } from 'vue-router'import TheSidebar from './components/TheSidebar'import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets'import { initWallet } from 'solana-wallets-vue'import { initWorkspace } from '@/composables'const route = useRoute()const wallets = [ new PhantomWalletAdapter(), new SolflareWalletAdapter(),]initWallet({ wallets, autoConnect: true })initWorkspace()</script>
The purpose of the code is to initialize the workspace consisting of connection, provider, wallet and program - and when user connects the wallet and tries to send a tweet - it must send the tweet using RPC API.
However, this code doesn't work. What DOES work, is the following send-tweet.js
content:
import { web3 } from "@coral-xyz/anchor";import { useWorkspace } from "@/composables";import { Tweet } from "@/models";export const sendTweet = async (topic, content) => { const { wallet, program, connection } = useWorkspace(); const tweet = web3.Keypair.generate(); const txSignature = await program.value.rpc.sendTweet(topic, content, { accounts: { author: wallet.value.publicKey, tweet: tweet.publicKey, systemProgram: web3.SystemProgram.programId, }, signers: [tweet], options: { preflightCommitment: program.value.provider.opts.preflightCommitment, commitment: program.value.provider.opts.commitment, }, }); // wait on the blockchain reflection of the transaction await connection.confirmTransaction(txSignature, program.value.provider.opts.commitment); // fetch the newly created account const tweetAccount = await program.value.account.tweet.fetch(tweet.publicKey); return new Tweet(tweet.publicKey, tweetAccount);};
...where we manually specify commitments in both(!) options
when calling program.value.rpc.sendTweet()
and in connection.confirmTransaction()
. If any of these is not specified, I'm getting this error:
Error: Simulation failed. Message: Transaction simulation failed: Blockhash not found.
This rises a couple of questions:
- Why do we need to specify commitment options in 4(!) places?
- If we already passed an
AnchorProvider
, with already setpreflightCommitment
andcommitment
properties, into ourProgram
, why do we need to specify them again when sending a tweet? I mean, the program should already know the values of these properties! - What is the perfect solution in this case, and that is the minimum subset of places where we must specify these properties?
Any help would be appreciated.