DEV Community

Cover image for Building a simple on-chain point of sale with Solana, Anchor and React
Fernando Mendez
Fernando Mendez

Posted on • Updated on • Originally published at fmendez.com

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

Note: All the code for this post can be found in 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:

 describe("ticketing-system", () => {
  const anchor = require("@project-serum/anchor");
  const assert = require("assert");

  const { SystemProgram } = anchor.web3;
  // Configure the client to use the local cluster.
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.TicketingSystem;
  const _ticketingSystem = anchor.web3.Keypair.generate();
  const tickets = [1111, 2222, 3333];

  it("Is initializes the ticketing system", async () => {
    const ticketingSystem = _ticketingSystem;
    await program.rpc.initialize(tickets, {
      accounts: {
        ticketingSystem: ticketingSystem.publicKey,
        user: provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
      },
      signers: [ticketingSystem],
    });

    const account = await program.account.ticketingSystem.fetch(
      ticketingSystem.publicKey
    );

    assert.ok(account.tickets.length === 3);
    assert.ok(
      account.tickets[0].owner.toBase58() ==
      ticketingSystem.publicKey.toBase58()
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

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., lib.rs). First, let's create the structs that represent both our Ticket and the TicketingSystem

#[account]
#[derive(Default)]
pub struct TicketingSystem {
    pub tickets: [Ticket; 3],
}

#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone, Copy)]
pub struct Ticket {
    pub owner: Pubkey,
    pub id: u32,
    pub available: bool,
    pub idx: u32,
}
Enter fullscreen mode Exit fullscreen mode

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:

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user)]
    pub ticketing_system: Account<'info, TicketingSystem>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct PurchaseTicket<'info> {
    #[account(mut)]
    pub ticketing_system: Account<'info, TicketingSystem>,
    pub user: Signer<'info>,
}
Enter fullscreen mode Exit fullscreen mode

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:

    pub fn initialize(ctx: Context<Initialize>, tickets: Vec<u32>) -> ProgramResult {
        let ticketingSystem = &mut ctx.accounts.ticketing_system;
        let owner = ticketingSystem.to_account_info().key;

        for (idx, ticket) in tickets.iter().enumerate() {
            ticketingSystem.tickets[idx] = Ticket {
                owner: *owner,
                id: *ticket,
                available: true,
                idx: idx as u32,
            };
        }
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

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

  ticketing-system
     Is initializes the ticketing system (422ms)


  1 passing (426ms)

  Done in 8.37s.
Enter fullscreen mode Exit fullscreen mode

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:

...
function App() {
  const wallet = useWallet();

  if (!wallet.connected) {
    return (
      <div className="main-container p-4">
        <div className="flex flex-col lg:w-1/4 sm:w-full md:w-1/2">
          <WalletMultiButton />
        </div>

      </div>
    );
  } else {
    return (
      <div className="main-container">
        <div className="border-b-4 border-brand-border self-stretch">
          <h1 className="font-bold text-4xl text-center p-4 text-brand-border">Ticket Sales</h1>
        </div>
        <Tickets />
      </div>
    );
  }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

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

This is what Tickets look like:

function Tickets() {
  const wallet = useWallet();

  const [tickets, setTickets] = useState<TicketInfo[]>([]);
  const initializeTicketingSystem = async () => {
    const provider = await getProvider((wallet as any) as NodeWallet);
    const program = new Program((idl as any) as Idl, programID, provider);

    try {
      await program.rpc.initialize(generateTickets(3), {
        accounts: {
          ticketingSystem: ticketingSystem.publicKey,
          user: provider.wallet.publicKey,
          systemProgram: SystemProgram.programId,
        },
        signers: [ticketingSystem],
      });
      const account = await program.account.ticketingSystem.fetch(
        ticketingSystem.publicKey
      );
      setTickets(account.tickets);
    } catch (err) {
      console.log("Transaction error: ", err);
    }
  };

  return (
    <div>
      {tickets.length === 0 && (
        <button className="bg-brand-btn rounded-xl font-bold text-xl m-4 p-2 hover:bg-brand-btn-active" onClick={initializeTicketingSystem}>
          Generate Tickets
        </button>
      )}
      {tickets.map((ticket) => (
        <Ticket
          key={ticket.id}
          ticket={ticket}
          ticketingSystem={ticketingSystem}
          setTickets={setTickets}
        />
      ))}
    </div>
  );
}

export default Tickets;
Enter fullscreen mode Exit fullscreen mode

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:

  ....
  const purchase = async (ticket: TicketInfo) => {
    const provider = await getProvider((wallet as any) as NodeWallet);
    const program = new Program((idl as any) as Idl, programID, provider);
    try {
      await program.rpc.purchase(ticket.id, ticket.idx, {
        accounts: {
          ticketingSystem: ticketingSystem.publicKey,
          user: provider.wallet.publicKey,
        },
      });

      const account = await program.account.ticketingSystem.fetch(
        ticketingSystem.publicKey
      );
      setTickets(account.tickets);
    } catch (err) {
      console.log("Transaction error: ", err);
    }
  };
  ....
Enter fullscreen mode Exit fullscreen mode

All the styled components look like this:

Ticket Sales
Tickets
Purchase

A gif showing it in action:

Using the app

You can try a live version ( pointing to the testnet.api ) here

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:

  ...
  #[account(init, payer = user, space = 64 + 64)]
  pub ticketing_system: Account<'info, TicketingSystem>,
  ...
Enter fullscreen mode Exit fullscreen mode

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 lib.rs 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.

Discussion (4)

Collapse
luisvonmuller profile image
Luís Von Muller

Nice work Mendez. :)

Collapse
fndomendez profile image
Fernando Mendez Author

Thanks!

Collapse
ntirinigasr profile image
Michael Ntiriniga Michael Senior

Wooow!! I'm impressed, great work

Collapse
fndomendez profile image
Fernando Mendez Author

Thank you!