Skip to content
Fernando Mendez

Building a simple on-chain point of sale with Solana, Anchor and React

rust, programming, solana, crypto, anchor, react3 min read

Note: All the code for this post can be found this github repo.

A few days ago, I started playing with the Solana blockchain. I was initially interested because it was built on rust (I freaking love rust!). To explore it, I decided to build a basic point of sales (POS) for event tickets.

I initially started reading the code on the Solana Program Library and experimenting but decided to go with Anchor just get started building something more quickly.

I'm not going to describe how to install Solana or Anchor. There is already a fantastic guide written here

The first thing I really love about Anchor is that I was able to start with a test-driven development approach. I started with the first test:

1describe("ticketing-system", () => {
2 const anchor = require("@project-serum/anchor");
3 const assert = require("assert");
5 const { SystemProgram } = anchor.web3;
6 // Configure the client to use the local cluster.
7 const provider = anchor.Provider.env();
8 anchor.setProvider(provider);
9 const program = anchor.workspace.TicketingSystem;
10 const _ticketingSystem = anchor.web3.Keypair.generate();
11 const tickets = [1111, 2222, 3333];
13 it("Is initializes the ticketing system", async () => {
14 const ticketingSystem = _ticketingSystem;
15 await program.rpc.initialize(tickets, {
16 accounts: {
17 ticketingSystem: ticketingSystem.publicKey,
18 user: provider.wallet.publicKey,
19 systemProgram: SystemProgram.programId,
20 },
21 signers: [ticketingSystem],
22 });
24 const account = await program.account.ticketingSystem.fetch(
25 ticketingSystem.publicKey
26 );
28 assert.ok( === 3);
29 assert.ok(
30[0].owner.toBase58() ==
31 ticketingSystem.publicKey.toBase58()
32 );
33 });

With this, I'm testing the ability to create 3 tickets, store it on-chain and ensure that all of them are owned by the program account.

To make the test pass, we have to work on the program account (e.g., First, let's create the structs that represent both our Ticket and the TicketingSystem

3pub struct TicketingSystem {
4 pub tickets: [Ticket; 3],
7#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone, Copy)]
8pub struct Ticket {
9 pub owner: Pubkey,
10 pub id: u32,
11 pub available: bool,
12 pub idx: u32,

The #[account] on the TicketingSystem automatically prepend the first 8 bytes of the SHA256 of the account’s Rust ident (e.g., what's inside the declare_id). This is a security check that ensures that a malicious actor could not just inject a different type and pretend to be that program account.

We are creating an array of Ticket, so we have to make it serializable. The other thing to note is that I'm specifying the owner to be of type Pubkey. The idea is that upon creation, the ticket will be initially owned by the program and when I make a purchase the ownership will be transferred.

The remaining structures:

2pub struct Initialize<'info> {
3 #[account(init, payer = user)]
4 pub ticketing_system: Account<'info, TicketingSystem>,
5 #[account(mut)]
6 pub user: Signer<'info>,
7 pub system_program: Program<'info, System>,
11pub struct PurchaseTicket<'info> {
12 #[account(mut)]
13 pub ticketing_system: Account<'info, TicketingSystem>,
14 pub user: Signer<'info>,

The #[derive(Accounts)] implements an Accounts deserializer. This applies any constraints specified by the #[account(...)] attributes. For instance, on the Initialize struct we have had the payer = user constrains specifying who's paying for the initialization cost (e.g., when the program is deploying).

The following code handles the actual initialization:

1pub fn initialize(ctx: Context<Initialize>, tickets: Vec<u32>) -> ProgramResult {
2 let ticketingSystem = &mut ctx.accounts.ticketing_system;
3 let owner = ticketingSystem.to_account_info().key;
5 for (idx, ticket) in tickets.iter().enumerate() {
6[idx] = Ticket {
7 owner: *owner,
8 id: *ticket,
9 available: true,
10 idx: idx as u32,
11 };
12 }
13 Ok(())
14 }

After some fiddling and debugging, I finally get a passing test with anchor test:

2 ✔ Is initializes the ticketing system (422ms)
5 1 passing (426ms)
7✨ Done in 8.37s.

Now that I have a list of on-chain Tickets I can retrieve, I want to see them. I decide to create a React app for this. Anchor already created an /app folder, let's use it.

The overall setup is very much like the one here, with the difference that I'm using Typescript.

The next React code will be shown without the imports. You can find the full code here:

The App.tsx contains code to detect if we're connected to a wallet or not:

2function App() {
3 const wallet = useWallet();
5 if (!wallet.connected) {
6 return (
7 <div className="main-container p-4">
8 <div className="flex flex-col lg:w-1/4 sm:w-full md:w-1/2">
9 <WalletMultiButton />
10 </div>
12 </div>
13 );
14 } else {
15 return (
16 <div className="main-container">
17 <div className="border-b-4 border-brand-border self-stretch">
18 <h1 className="font-bold text-4xl text-center p-4 text-brand-border">Ticket Sales</h1>
19 </div>
20 <Tickets />
21 </div>
22 );
23 }
26export default App;

I created a few components for Ticket and Tickets. I also used tailwindcss to style them.

This is what Tickets look like:

1function Tickets() {
2 const wallet = useWallet();
4 const [tickets, setTickets] = useState<TicketInfo[]>([]);
5 const initializeTicketingSystem = async () => {
6 const provider = await getProvider((wallet as any) as NodeWallet);
7 const program = new Program((idl as any) as Idl, programID, provider);
9 try {
10 await program.rpc.initialize(generateTickets(3), {
11 accounts: {
12 ticketingSystem: ticketingSystem.publicKey,
13 user: provider.wallet.publicKey,
14 systemProgram: SystemProgram.programId,
15 },
16 signers: [ticketingSystem],
17 });
18 const account = await program.account.ticketingSystem.fetch(
19 ticketingSystem.publicKey
20 );
21 setTickets(;
22 } catch (err) {
23 console.log("Transaction error: ", err);
24 }
25 };
27 return (
28 <div>
29 {tickets.length === 0 && (
30 <button className="bg-brand-btn rounded-xl font-bold text-xl m-4 p-2 hover:bg-brand-btn-active" onClick={initializeTicketingSystem}>
31 Generate Tickets
32 </button>
33 )}
34 { => (
35 <Ticket
36 key={}
37 ticket={ticket}
38 ticketingSystem={ticketingSystem}
39 setTickets={setTickets}
40 />
41 ))}
42 </div>
43 );
46export default Tickets;

Here, we provide a Generate Tickets button that will initialize the tickets on-chain. These RPC calls could be moved to an API file, but I'll keep there since it is the only place that needs it. The code for the Ticket is similar in structure. Here will call the purchase RPC call:

2 const purchase = async (ticket: TicketInfo) => {
3 const provider = await getProvider((wallet as any) as NodeWallet);
4 const program = new Program((idl as any) as Idl, programID, provider);
5 try {
6 await program.rpc.purchase(, ticket.idx, {
7 accounts: {
8 ticketingSystem: ticketingSystem.publicKey,
9 user: provider.wallet.publicKey,
10 },
11 });
13 const account = await program.account.ticketingSystem.fetch(
14 ticketingSystem.publicKey
15 );
16 setTickets(;
17 } catch (err) {
18 console.log("Transaction error: ", err);
19 }
20 };
21 ....

All the styled components look like this:

Ticket Sales! Tickets! Purchase!

For fun, I added a QR code that's based on the ticket number and the account that made the purchase.

Overall, this was a fun experiment. Based on my initial experimentation using the Solana SDK directly, there's a lot that Anchor is abstracting away. There's also good practices built into it (e.g., the 8 bytes discriminator for the program's account, lack of order when accessing accounts, etc.). I'll be spending more time with both Anchor and the Solana SDK itself to make sure I understand what's being abstracted away.

Finally, there are a few troubleshooting tips that might help you when using Anchor.

  • Remember to anchor build and anchor deploy before running anchor test. That ensures that you have the latest bytecode on the runtime. You will encounter a serialization error if you don't.
  • When you encounter custom errors such as this: "Transaction simulation failed: Error processing Instruction 0: custom program error: 0x66". Convert the number from hex -> integer, if the number is >=300 it's an error from your program, look into the errors section of the idl that gets generated when building your anchor project. If it is < 300, then search the matching error number here
  • When you get this type of error: "error: Error: 163: Failed to deserialize the account". Very often it's because you haven't allocated enough space (anchor tried to write the account back out to storage and failed). This is solved by allocating more space during the initialization.

For example, had to bump this to 64 to solve the issue. Was initially at 8:

2 #[account(init, payer = user, space = 64 + 64)]
3 pub ticketing_system: Account<'info, TicketingSystem>,
4 ...

Alternatively (and the recommended option from what I've gathered) is to leave the space out for Anchor to calculate it for you. The exception is if you're dealing with a complex of Custom types that Anchor can't calculate for some reason.

  • If you for whatever reason you need to generate a new program ID (e.g., a fail deployment to devent or testdeve made that account address in use and is not upgradeable). You can simply delete the /deploy folder under target (e.g /root-of-your-anchor-project/target/deploy) and run anchor build again. That will regenerate the /deploy folder. After that, you just need to run this from your root project directory solana address -k target/deploy/name-of-your-file-keypair.json. You can take that output and upgrade both the declare_id() in your and Anchor.toml with the new program ID. Finally, you have to run anchor build again to rebuild with the new program ID.

I still have a lot to explore, I find both Anchor and the current Solana ecosystem very exciting. Will continue to post my progress. Until the next time.

© 2022 by Fernando Mendez. All rights reserved.
Theme by LekoArts