Build React dApp with Account Abstraxion

A working demo can be found here in the `apps/demo-app` folder.

Goal

The intention here is to help you setup a basic dapp using the Abstraxion library showcasing both query and transaction submission.

Requirements

Setup Project

For this example we will use nextjs to scaffold the project

Run the following commands in your terminal:

# Generate project along with settings to avoid wizard.
npx create-next-app@latest nextjs-xion-abstraxion-example --use-npm --ts --eslint --tailwind --app  --src-dir --import-alias "@/*"

# Enter application directory
cd nextjs-xion-abstraxion-example

Add the Abstraxion library to the project:

npm i @burnt-labs/abstraxion

Start the project in developer mode:

npm run dev

Open https://localhost:3000 in a web browser and you will see a fancy animated react logo.

Setup Abstraxion Library

Replace the contents of src/app/layout.tsx with the following body:

src/app/layout.tsx
"use client";
import { Inter } from 'next/font/google'
import './globals.css'
import {AbstraxionProvider} from "@burnt-labs/abstraxion";

import "@burnt-labs/abstraxion/dist/index.css";
import "@burnt-labs/ui/dist/index.css";

const inter = Inter({ subsets: ['latin'] })

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AbstraxionProvider
          config={{
            contracts: ["xion1z70cvc08qv5764zeg3dykcyymj5z6nu4sqr7x8vl4zjef2gyp69s9mmdka"],
          }}
        >
          {children}
        </AbstraxionProvider>
      </body>
    </html>
  )
}

Without the "use client"; directive at the top, you will get a error

The AbstraxionProvider is required to provide context for the useAbstraxionAccount and `useAbstraxionSigningClient` hooks.

Be sure to provide the array of contract addresses your DAPP intends to interact with.

Add Hooks to the homepage

Replace the contents of `src/app/page.tsx` with the following:

src/app/page.tsx
"use client";
import {
  Abstraxion,
  useAbstraxionAccount,
  useModal
} from "@burnt-labs/abstraxion";
import { Button } from "@burnt-labs/ui";
import { useEffect } from "react";

export default function Page(): JSX.Element {
  // Abstraxion hooks
  const { data: { bech32Address }, isConnected, isConnecting } = useAbstraxionAccount();

  // General state hooks
  const [, setShow] = useModal();

  // watch isConnected and isConnecting
  // only added for testing
  useEffect(() => {
    console.log({ isConnected, isConnecting });
  }, [isConnected, isConnecting])

  return (
      <main className="m-auto flex min-h-screen max-w-xs flex-col items-center justify-center gap-4 p-4">
        <h1 className="text-2xl font-bold tracking-tighter text-black dark:text-white">
          Abstraxion
        </h1>
        <Button
            fullWidth
            onClick={() => { setShow(true) }}
            structure="base"
        >
          {bech32Address ? (
              <div className="flex items-center justify-center">VIEW ACCOUNT</div>
          ) : (
              "CONNECT"
          )}
        </Button>
        {
          bech32Address &&
            <div className="border-2 border-primary rounded-md p-4 flex flex-row gap-4">
              <div className="flex flex-row gap-6">
                <div>
                  address
                </div>
                <div>
                  {bech32Address}
                </div>
              </div>
            </div>
        }
        <Abstraxion onClose={() => setShow(false)} />
      </main>
  );
}

This will give a a button that initiates a meta account using social login. Click the `CONNECT` button and try it out!

Transaction submission

Querying the chain wouldn't be of much use without a mechanism to alter chain state. Let's do that now.

Refresh the contents of src/app/page.tsx with the following:

src/app/page.tsx
"use client";
import Link from "next/link";
import { useState } from "react";
import {
  Abstraxion,
  useAbstraxionAccount,
  useAbstraxionSigningClient,
} from "@burnt-labs/abstraxion";
import { Button } from "@burnt-labs/ui";
import "@burnt-labs/ui/dist/index.css";
import type { ExecuteResult } from "@cosmjs/cosmwasm-stargate";
import { seatContractAddress } from "./layout";

type ExecuteResultOrUndefined = ExecuteResult | undefined;
export default function Page(): JSX.Element {
  // Abstraxion hooks
  const { data: account } = useAbstraxionAccount();
  const { client } = useAbstraxionSigningClient();

  // General state hooks
  const [isOpen, setIsOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [executeResult, setExecuteResult] =
    useState<ExecuteResultOrUndefined>(undefined);

  const blockExplorerUrl = `https://explorer.burnt.com/xion-testnet-1/tx/${executeResult?.transactionHash}`;

  function getTimestampInSeconds(date: Date | null) {
    if (!date) return 0;
    const d = new Date(date);
    return Math.floor(d.getTime() / 1000);
  }

  const now = new Date();
  now.setSeconds(now.getSeconds() + 15);
  const oneYearFromNow = new Date();
  oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);

  async function claimSeat() {
    setLoading(true);
    const msg = {
      sales: {
        claim_item: {
          token_id: String(getTimestampInSeconds(now)),
          owner: account.bech32Address,
          token_uri: "",
          extension: {},
        },
      },
    };

    try {
      const claimRes = await client?.execute(
        account.bech32Address,
        seatContractAddress,
        msg,
        {
          amount: [{ amount: "0", denom: "uxion" }],
          gas: "500000",
        },
        "", // memo
        [],
      );

      setExecuteResult(claimRes);
    } catch (error) {
      // eslint-disable-next-line no-console -- No UI exists yet to display errors
      console.log(error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <main className="m-auto flex min-h-screen max-w-xs flex-col items-center justify-center gap-4 p-4">
      <h1 className="text-2xl font-bold tracking-tighter text-white">
        ABSTRAXION
      </h1>
      <Button
        fullWidth
        onClick={() => {
          setIsOpen(true);
        }}
        structure="base"
      >
        {account.bech32Address ? (
          <div className="flex items-center justify-center">VIEW ACCOUNT</div>
        ) : (
          "CONNECT"
        )}
      </Button>
      {client ? (
        <Button
          disabled={loading}
          fullWidth
          onClick={() => {
            void claimSeat();
          }}
          structure="base"
        >
          {loading ? "LOADING..." : "CLAIM SEAT"}
        </Button>
      ) : null}
      <Abstraxion
        isOpen={isOpen}
        onClose={() => {
          setIsOpen(false);
        }}
      />
      {executeResult ? (
        <div className="flex flex-col rounded border-2 border-black p-2 dark:border-white">
          <div className="mt-2">
            <p className="text-zinc-500">
              <span className="font-bold">Transaction Hash</span>
            </p>
            <p className="text-sm">{executeResult.transactionHash}</p>
          </div>
          <div className="mt-2">
            <p className=" text-zinc-500">
              <span className="font-bold">Block Height:</span>
            </p>
            <p className="text-sm">{executeResult.height}</p>
          </div>
          <div className="mt-2">
            <Link
              className="text-black underline visited:text-purple-600 dark:text-white"
              href={blockExplorerUrl}
              target="_blank"
            >
              View in Block Explorer
            </Link>
          </div>
        </div>
      ) : null}
    </main>
  );
}

Quick Note on Fee Config

Inside the claimSeat function above, the .execute() is being called as such:

const claimRes = await client?.execute(
  account.bech32Address,
  seatContractAddress,
  msg,
  {
    amount: [{ amount: "0", denom: "uxion" }],
    gas: "500000",
  },
  "", // memo
  [],
);

The fourth parameter in the above function call represents the fee config - replacing the object with auto here will allow the SDK to handle fee configuration for you, eg.,

const claimRes = await client?.execute(
  account.bech32Address,
  seatContractAddress,
  msg,
  auto,
  "", // memo
  [],
);

If everything is successful you should see transaction results as shown above.

Summary

We accomplished several things:

  1. Setup a Next.js Project Start by generating a Next.js project using npx create-next-app@latest.

  2. Adding the Abstraxion Library Add the Abstraxion library (@burnt-labs/abstraxion) to the project via npm.

  3. Setup AbstraxionProvider In the layout file, setup the AbstraxionProvider to provide context for the useAbstraxionAccount and useAbstraxionSigningClient hooks.

  4. Landing Page Setup Modify the home page content to allow initiating a meta account through a social login with a button.

  5. Submit a Transaction Alter the chain's state by submitting a transaction.

These basic components are the majority of what is needed to create and deploy a successful dapp! Feel free to reach out to us on discord or on our Github.

Last updated