Utilizing Solana pay for consumer products

A checkout page using Solana pay

Utilizing Solana pay for consumer products

Introduction:

I've taken on the challenge from Superteam Nigeria and chosen to build a checkout experience using Solana Pay! 🛒💳

In this article, I will give you a walkthrough.

For a sneak peek, here's the checkout page: Link

Problem Statement:

In the rapidly evolving landscape of modern e-commerce, fast and cost-effective payment solutions are non-negotiable. Solana Pay—a game-changing blockchain payment system designed to revolutionize the way we handle digital transactions.

Why Solana?

  • Lightning-Fast Transactions: Solana processes payments at speeds comparable to in-store card payments, eliminating frustrating delays.

  • Minimal Fees: Transaction costs on Solana are a fraction of a cent, erasing the need for hefty gas fees seen on other blockchains.

As of now, Solana boasts impressive stats:

  • Transaction Speed: 2,070 transactions per second.

  • Cost Efficiency: An average transaction cost of $0.00025.

  • Massive Transaction Volume: Nearly 62.3 billion transactions and counting.

This article explores how Solana Pay powers a secure and efficient scan-to-pay page, shaping the future of cryptocurrency payments in e-commerce.

How it works:

Solana Pay harnesses the speed and minimal fees of the Solana network for real-world transactions. Any shop or market stall can now accept payments effortlessly, with no need for third-party services or credit card processing fees. The simplicity and efficiency are truly remarkable!

Here's how the checkout page works:

  1. User selects their desired chocolate.

  2. The checkout page displays a QR code.

  3. The user scans the QR code using a mobile wallet app and confirms the payment.

  4. We instantly receive the confirmation and hand over their chocolates.

It's that fast and that easy—Solana Pay in action!

Setup to test:

Setting up a Wallet

If we want to test the Checkout page like the above display, our first step is setting up a wallet. Much like wallets on other blockchain networks, a Solana wallet allows you to keep track of your balances and seamlessly interact with various applications. If you're already equipped with a Solana wallet, feel free to skip ahead!

The wallet of choice for many is Phantom, renowned for its user-friendly interface and robust features. You can easily download Phantom from their official website: https://phantom.app/download. It's also available as a browser extension for Chrome, Brave, Firefox, and Edge, ensuring compatibility across a wide range of browsers. Of course, if you have a preference for a different Solana wallet for any specific reason, you're welcome to explore other options—all Solana wallets are compatible with our journey.

Click on "Create a new wallet." But, here's a quick note on terminology: in the Solana world, the wallet you create here is also referred to as an "Account." We'll delve deeper into the account model of Solana later, so don't be surprised if you come across this term.

The next step is to set up a password, which is exclusively for the Phantom extension.

Following that, you'll encounter your Secret Recovery Phrase. If you've dabbled in the realm of blockchain before, this should ring a bell! Your Secret Recovery Phrase consists of 12 random words. It serves as your master key to access all the wallets you create within Phantom. For instance, you can use it to import these wallets into another wallet of your choice. Keep this phrase guarded like a treasure; sharing it would grant access to all your wallets. Be sure to save it in a secure place; you'll need it if you decide to import your wallets elsewhere.

Once your setup is complete, you'll find your wallet right there in Phantom:

Before we do anything, let's switch over to Devnet. On Devnet, all tokens are purely for practice and hold no real-world value, making it an excellent environment for learning. Plus, we can freely send Solana to ourselves, which is quite handy!

Here's how to make the switch in Phantom:

  1. Open Phantom and locate the Settings option (it's the gear icon in the bottom right).

  2. Choose "Change Network" from the menu and then select "Devnet".

Getting some USDC-Dev

You might be familiar with these tokens, often referred to as altcoins or fungible tokens. However, in the realm of Solana, they go by the name of SPL tokens, where SPL stands for Solana Program Library. These tokens can be securely stored in our wallets and seamlessly used for transactions within the Solana network.

Among these tokens, some are designed to maintain a fixed value equivalent to a fiat currency, typically $1. This stability is ensured because someone commits to buying and selling them at precisely $1, usually by holding an equal amount of dollars in reserve as there are tokens. This remarkable feature allows us to conduct blockchain payments using tokens with a guaranteed value of exactly $1, often referred to as stablecoins.

One such stablecoin is USDC, which is widely available on various blockchains, including Solana. However, since we're operating on Solana's devnet, we'll be utilizing a token known as USDC-Dev.

Now, let's acquire some USDC-Dev through a token faucet: https://spl-token-faucet.com/?token-name=USDC-Dev.

On the webpage, you'll find a button to connect your wallet via your browser wallet. Make sure you connect using the buyer account.

Once connected, you'll have the opportunity to airdrop yourself some of these tokens:

$1000 should be enough for our testing, but you can increase that amount if you need to!

Now click the “Get USDC-Dev” and your browser wallet should show a transaction:

Remember how fast Solana is? Well, brace yourself for some lightning speed!

Now, check Phantom, and you'll be greeted by the sight of your balance.

And now you can go ahead, pick your chocolates :), click on checkout and scan, to get the presentation on the remarkable efficiency of Solana Pay.

Code Highlights:

Solana transactions are versatile. They handle multiple instructions and ensure a full or zero execution guarantee. Instructions can transmit data to any Solana program. For instance:

  • We can have buyers send USDC to several recipients or split SOL and USDC between recipients.

  • USDC can go to the shop, and the shop can send an NFT or a loyalty coupon in return.

  • Discounts can apply, combining USDC with collected loyalty coupons.

Below is the code(makeTransaction.ts) that does the complicated part of the transaction requests: returning a serialized transaction and handling the GET request, so we’ll be able to use our API with mobile wallets.

makeTransaction.ts

import {
  createTransferCheckedInstruction,
  getAssociatedTokenAddress,
  getMint,
} from "@solana/spl-token";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
  clusterApiUrl,
  Connection,
  PublicKey,
  Transaction,
} from "@solana/web3.js";
import { NextApiRequest, NextApiResponse } from "next";
import { shopAddress, usdcAddress } from "../../lib/addresses";
import calculatePrice from "../../lib/calculatePrice";

export type MakeTransactionInputData = {
  account: string;
};

type MakeTransactionGetResponse = {
  label: string;
  icon: string;
};

export type MakeTransactionOutputData = {
  transaction: string;
  message: string;
};

type ErrorOutput = {
  error: string;
};

function get(res: NextApiResponse<MakeTransactionGetResponse>) {
  res.status(200).json({
    label: "Choco Inc",
    icon: "https://freesvg.org/img/chocolate-publicdomainvectors.png",
  });
}

async function post(
  req: NextApiRequest,
  res: NextApiResponse<MakeTransactionOutputData | ErrorOutput>
) {
  try {
    // We pass the selected items in the query, calculate the expected cost
    const amount = calculatePrice(req.query);
    if (amount.toNumber() === 0) {
      res.status(400).json({ error: "Can't checkout with charge of 0" });
      return;
    }

    // We pass the reference to use in the query
    const { reference } = req.query;
    if (!reference) {
      res.status(400).json({ error: "No reference provided" });
      return;
    }

    // We pass the buyer's public key in JSON body
    const { account } = req.body as MakeTransactionInputData;
    if (!account) {
      res.status(40).json({ error: "No account provided" });
      return;
    }
    const buyerPublicKey = new PublicKey(account);
    const shopPublicKey = shopAddress;

  // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
    const network = WalletAdapterNetwork.Devnet;
    const endpoint = clusterApiUrl(network);
    const connection = new Connection(endpoint);

    // Get details about the USDC token
    const usdcMint = await getMint(connection, usdcAddress);
    // Get the buyer's USDC token account address
    const buyerUsdcAddress = await getAssociatedTokenAddress(
      usdcAddress,
      buyerPublicKey
    );
    // Get the shop's USDC token account address
    const shopUsdcAddress = await getAssociatedTokenAddress(
      usdcAddress,
      shopPublicKey
    );

    // Get a recent blockhash to include in the transaction
    const { blockhash } = await connection.getLatestBlockhash("finalized");

    const transaction = new Transaction({
      recentBlockhash: blockhash,
      // The buyer pays the transaction fee
      feePayer: buyerPublicKey,
    });

    // Create the instruction to send USDC from the buyer to the shop
    const transferInstruction = createTransferCheckedInstruction(
      buyerUsdcAddress, // source
      usdcAddress, // mint (token address)
      shopUsdcAddress, // destination
      buyerPublicKey, // owner of source address
      amount.toNumber() * 10 ** usdcMint.decimals, // amount to transfer (in units of the USDC token)
      usdcMint.decimals // decimals of the USDC token
    );

    // Add the reference to the instruction as a key
    // This will mean this transaction is returned when we query for the reference
    transferInstruction.keys.push({
      pubkey: new PublicKey(reference),
      isSigner: false,
      isWritable: false,
    });

    // Add the instruction to the transaction
    transaction.add(transferInstruction);

    // Serialize the transaction and convert to base64 to return it
    const serializedTransaction = transaction.serialize({
      // We will need the buyer to sign this transaction after it's returned to them
      requireAllSignatures: false,
    });
    const base64 = serializedTransaction.toString("base64");

    // Insert into database: reference, amount

    // Return the serialized transaction
    res.status(200).json({
      transaction: base64,
      message: "Thanks for your order! 🍫",
    });
  } catch (err) {
    console.error(err);

    res.status(500).json({ error: "error creating transaction" });
    return;
  }
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<
    MakeTransactionGetResponse | MakeTransactionOutputData | ErrorOutput
  >
) {
  if (req.method === "GET") {
    return get(res);
  } else if (req.method === "POST") {
    return await post(req, res);
  } else {
    return res.status(405).json({ error: "Method not allowed" });
  }
}

Below is checkout.tsx For updating the QR code, it points to /makeTransaction API. The checkout page uses transaction requests functionality to make it work for customers in our shop as well as online.

checkout.tsx

import {
  createQR,
  encodeURL,
  TransferRequestURLFields,
  findReference,
  validateTransfer,
  FindReferenceError,
  ValidateTransferError,
  TransactionRequestURLFields,
} from "@solana/pay";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js";
import { useRouter } from "next/router";
import { useEffect, useMemo, useRef } from "react";
import BackLink from ".././components/BackLink";
import PageHeading from ".././components/PageHeading";
import { shopAddress, usdcAddress } from ".././lib/addresses";
import calculatePrice from ".././lib/calculatePrice";

export default function Checkout() {
  const router = useRouter();

  // ref to a div where we'll show the QR code
  const qrRef = useRef<HTMLDivElement>(null);

  const amount = useMemo(() => calculatePrice(router.query), [router.query]);

  // Unique address that we can listen for payments to
  const reference = useMemo(() => Keypair.generate().publicKey, []);

  // Read the URL query (which includes our chosen products)
  const searchParams = new URLSearchParams({ reference: reference.toString() });
  for (const [key, value] of Object.entries(router.query)) {
    if (value) {
      if (Array.isArray(value)) {
        for (const v of value) {
          searchParams.append(key, v);
        }
      } else {
        searchParams.append(key, value);
      }
    }
  }

  // Get a connection to Solana devnet
  // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
  const network = WalletAdapterNetwork.Devnet;
  const endpoint = clusterApiUrl(network);
  const connection = new Connection(endpoint);

  // Show the QR code
  useEffect(() => {
    // window.location is only available in the browser, so create the URL in here
    const { location } = window;
    const apiUrl = `${location.protocol}//${
      location.host
    }/api/makeTransaction?${searchParams.toString()}`;
    const urlParams: TransactionRequestURLFields = {
      link: new URL(apiUrl),
      label: "Choco Inc",
      message: "Thanks for your order! 🍫",
    };
    const solanaUrl = encodeURL(urlParams);
    const qr = createQR(solanaUrl, 300, "transparent");
    if (qrRef.current && amount.isGreaterThan(0)) {
      qrRef.current.innerHTML = "";
      qr.append(qrRef.current);
    }
  });

  // Check every 0.5s if the transaction is completed
  useEffect(() => {
    const interval = setInterval(async () => {
      try {
        // Check if there is any transaction for the reference
        const signatureInfo = await findReference(connection, reference, {
          finality: "confirmed",
        });
        // Validate that the transaction has the expected recipient, amount and SPL token
        await validateTransfer(
          connection,
          signatureInfo.signature,
          {
            recipient: shopAddress,
            amount,
            splToken: usdcAddress,
            reference,
          },
          { commitment: "confirmed" }
        );
        router.push("/confirmed");
      } catch (e) {
        if (e instanceof FindReferenceError) {
          // No transaction found yet, ignore this error
          return;
        }
        if (e instanceof ValidateTransferError) {
          // Transaction is invalid
          console.error("Transaction is invalid", e);
          return;
        }
        console.error("Unknown error", e);
      }
    }, 500);
    return () => {
      clearInterval(interval);
    };
  }, []);

  return (
    <div className="flex flex-col items-center gap-8 mt-20 text-white">
      <BackLink href="/">Go back</BackLink>

      <PageHeading>Checkout: ${amount.toString()}</PageHeading>

      {/* div added to display the QR code */}
      <div className="bg-white opacity-90" ref={qrRef} />
    </div>
  );
}

If you would like to play around with the code.
Here's a GitHub link. The guideline on how to run the app is in the README.md.

Using Solana Pay to Revolutionize Payments

Solana Pay is more than just a concept; it's a groundbreaking protocol and a set of reference implementations designed to empower developers to seamlessly integrate decentralized payments into their applications and services. The Solana blockchain, known for confirming transactions in less than a second with an average cost of $0.0005, has paved the way for a future where digital payments are fast, cost-effective, and entirely free from intermediaries.

Supporting Wallets

Solana Pay is accessible through a variety of wallets, making it convenient for both users and businesses. Supported wallets include:

  • Phantom (iOS, Android)

  • Solflare (iOS, Android)

  • Glow (iOS, Android)

  • Decaf Wallet (iOS, Android)

  • Espresso Cash (iOS, Android)

  • Ottr (iOS, Android)

  • Ultimate (iOS, Android)

  • Tiplink (Web)

How to Utilize Solana Pay

  1. Accept Payments in Your Web App: Implement the @solana/pay JavaScript SDK to start accepting payments in your web application effortlessly.

  2. Accept Payments in Person: Utilize the open-source Solana Pay Point of Sale app to begin accepting in-person payments seamlessly.

Getting Involved

Solana Pay is an open standard with a focus on facilitating commerce on the Solana blockchain. If you're interested in contributing to this ecosystem, there are numerous ways to get involved:

  • SuperteamDAO - A community that helps Solana projects launch and grow in ascending markets. If you are in Nigeria, join SuperteamNG.

  • Hackathon Projects: Join the Solana Grizzlython Hackathon, featuring a dedicated Payments track presented by Stripe. Consider innovative ideas to shape the future of payments.

  • eCommerce Platform Integrations: Help more merchants adopt Solana Pay by creating easy-to-use integrations for popular eCommerce platforms like WooCommerce, Magento, BigCommerce, Wix, Squarespace, and others. Solana Labs has initiated a reference implementation for Shopify, offering insights into how such integrations can work.

  • Other Possible Projects: Explore opportunities such as developing mobile SDKs, crafting checkout UX components, or proposing your ideas to the community by opening an issue.

Ready to Pioneer the Future of Payments?

Explore Solana Pay today and witness the transformative potential it holds for payments. Share your thoughts, suggestions, and feedback as we collectively shape the evolution of this groundbreaking technology. If you're eager to be part of the revolution, Solana Pay is your gateway to pioneering a new era in cryptocurrency payments.

For detailed implementation guidelines and documentation, check out the Solana Pay documentation.

Thank You:

I extend my heartfelt thanks to Superteam Nigeria for this opportunity to showcase Solana Pay's potential. Together, we're pioneering a new era in cryptocurrency payments, and I'm eager to see where this journey leads.

Thank you for being part of this exciting adventure! 🚀💼
Give a thumbs up 👍 and share with friends, we don't want to miss out on this amazing evolution 😉