Aptos Dapp Journey - Part 3

Using the TypeScript SDK for data queries and interactions

·

16 min read

I've been grappling with a question lately: how do I actually talk to the blockchain? It's time to explore ways of getting data and interacting with smart contracts. This journey into web3 has been quite a ride. I started by setting up a project template, then dove into smart contracts.

Now, I'm facing my next challenge: connecting my app to the blockchain.

In this part of my adventure, I'm aiming to:

  1. Discover tools for fetching on-chain data

  2. Learn how to read from smart contracts

  3. Figure out how to write to the blockchain

It's not always clear-cut, and I'm still learning. I'll share what I've found about SDKs, other data sources, and making transactions. Let's see what we can uncover together.

Exploring SDKs

As I dive deeper into blockchain development, I've hit a roadblock. How do I efficiently interact with the blockchain without reinventing the wheel? This is where SDKs, or Software Development Kits, come into play, which are tools designed to make a developer's life easier. They're like a Swiss Army knife for blockchain interaction, offering pre-built functions and utilities.

But why use them? SDKs can significantly speed up development. They handle many low-level details, allowing me to focus on building my app's unique features. In my search for solutions, I discovered that Aptos offers SDKs for various languages:

  • Unity SDK for game development

  • C++ / Unreal SDK for more game options

  • Python and Rust SDKs for backend work

  • Kotlin SDK for mobile development

  • TypeScript SDK for full stack applications

The TypeScript SDK caught my eye. It is the natural choice since I'm building a web app with React and TypeScript. It's also the most popular and recommended option, which gives me confidence.

It's well-documented and includes key features such as key generation, transaction signing, and submission. The Github repository offers a thorough set of examples covering various use cases. Additionally, it includes built-in support for NFT operations.

However, while the TypeScript SDK offers a robust set of tools, it's just one piece of the puzzle. To truly understand how to interact with the Aptos blockchain, I need to explore different ways of getting on-chain data.

Different ways to load data from chain

SDKs are just one path to on-chain data. There's a whole ecosystem of tools and APIs to explore. First, there's the Node API. It's the foundation on which most SDKs are built. Understanding these API calls has been crucial for me to grasp what's happening under the hood of the SDK I'm using.

Then, I discovered the Indexer API. It's been a game-changer for more complex queries. While SDKs are great for common tasks, the Indexer API lets me craft custom queries for specific data needs. It's like having a powerful magnifying glass for blockchain data. During development, I've found two tools incredibly handy:

  1. The command-line interface (CLI) has become my go-to for quick actions from the terminal. There's no need to switch away from my editor, and I can interact with the blockchain with a few keystrokes.

  2. I use the Explorer when I need a visual representation of the data. It's a fast way to view and verify data in a user-friendly interface.

For my project, I'm leaning towards a combination of Surf by Thala Labs, built on top of the Aptos TypeScript SDK, which offers type safety and ease of use, and the Indexer API with GraphQL for our more advanced queries. I’m building a Product Hunt-style dApp on the blockchain. Here's what I will implement:

  1. Users can mint products as NFTs

  2. Users can upvote products

  3. Display a list of product NFTs

  4. Show recent activities from on-chain events

This mix of features requires both simple and complex data fetching. While the Indexer API will be crucial for our more intricate queries, I want to start with the basics. Let's begin by exploring how to fetch data using the TypeScript SDK. This will be a solid foundation for diving into more advanced querying techniques.

Fetching data with the TypeScript SDK

As I started working with the Aptos TypeScript SDK, I found it helpful to test API calls before integrating them into the frontend. I created the following script at scripts/ts/surf_product_nft_mint.ts , to iterate quickly and understand the SDK's behavior:

import {
  Account,
  Aptos,
  AptosConfig,
  Ed25519PrivateKey,
  Network,
  NetworkToNetworkName,
} from "@aptos-labs/ts-sdk"
import { createSurfClient } from "@thalalabs/surf"
import dotenv from "dotenv"
import { ABI } from "../../frontend/utils/abi-product_nft"
dotenv.config()

const APTOS_NETWORK: Network =
  NetworkToNetworkName[process.env.VITE_APP_NETWORK ?? Network.TESTNET]

const config = new AptosConfig({ network: APTOS_NETWORK })
const aptos = new Aptos(config)
const surfProductNFT = createSurfClient(aptos).useABI(ABI)

if (!process.env.PRIVATE_KEY) throw new Error("PRIVATE_KEY not found")

// Use existing user account
const privateKey = new Ed25519PrivateKey(process.env.PRIVATE_KEY)
const user = Account.fromPrivateKey({
  privateKey: privateKey,
})

const main = async () => {
  console.log("=== Address ===\n")
  console.log(`User's address is: ${user.accountAddress}`)

  console.log("\n=== Product NFT Mint ===\n")

  /**
   * View Function
   */
  const [collectionName] = await surfProductNFT.view.get_collection_name({
    functionArguments: [],
    typeArguments: [],
  })
  console.log("Collection Name: ", collectionName)

  /**
   * Entry Function
   */
  const PRODUCT_NAME = "Open Forge 1"
  const TOKEN_URI =
    "https://gateway.irys.xyz/DECscf3teYKE86hM8SmiUxPYmnfedQNRGiQhPmofBNRo"
  await mintProductNFT(
    PRODUCT_NAME,
    "Connecting Aptos builders and backers",
    TOKEN_URI
  )
  await upvoteProduct(PRODUCT_NAME)
}

const mintProductNFT = async (
  name: string,
  description: string,
  uri: string
) => {
  const result = await surfProductNFT.entry.mint_product({
    functionArguments: [name, description, uri],
    typeArguments: [],
    account: user,
  })

  const tx = await aptos.waitForTransaction({
    transactionHash: result.hash,
  })
  console.log(`Mint transaction status: ${tx.success ? "Success" : "Failed"}`)
  console.log(
    `View transaction on explorer: https://explorer.aptoslabs.com/txn/${tx.hash}?network=${process.env.VITE_APP_NETWORK}`
  )
}

const upvoteProduct = async (productName: string) => {
  const result = await surfProductNFT.entry.upvote_product({
    functionArguments: [productName],
    typeArguments: [],
    account: user,
  })

  const tx = await aptos.waitForTransaction({
    transactionHash: result.hash,
  })
  console.log(`Upvote transaction status: ${tx.success ? "Success" : "Failed"}`)
  console.log(
    `View transaction on explorer: https://explorer.aptoslabs.com/txn/${tx.hash}?network=${process.env.VITE_APP_NETWORK}`
  )
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

Let’s explore important parts of this code.

First, I initialized the Aptos client and created a client for my ProductNFT contract. I use the Surf library to ensure my contract methods are type-safe.

Next, I used an existing private key to create a user account. This allows me to interact with the blockchain and sign transactions.

After I finished the initialization, I used the ProductNFT client inside my main function to interact with the blockchain.

Reading data from the chain via view methods doesn’t require paying gas fees and is done with the following code:

await surfProductNFT.view.get_collection_name({
  functionArguments: [],
  typeArguments: [],
})

Writing data to the chain via transactions costs gas fees and is done with this code:

await surfProductNFT.entry.mint_product({
  functionArguments: [name, description, uri],
  typeArguments: [],
  account: user,
})

To wait for the confirmation of a transaction to ensure it was processed before I move on, I used this code:

await aptos.waitForTransaction({
  transactionHash: result.hash,
})

Setting up the frontend

I'm leveraging the create-aptos-template as a foundation. This template provides a solid starting point for Aptos-based applications. I'm particularly inspired by the aptos_friend example, which demonstrates practical implementation of Aptos interactions in a frontend environment.

Here is what's inside the frontend/utils/aptosClient.ts file, where we set up our Aptos client for frontend use:

import { Aptos, AptosConfig } from "@aptos-labs/ts-sdk"
import { NETWORK } from "@/lib/constants"

const aptos = new Aptos(new AptosConfig({ network: NETWORK }))

// Reuse same Aptos instance to utilize cookie based sticky routing
export function aptosClient() {
  return aptos
}

How to fetch NFT data

Let’s see how I display NFT data on the Explore page at frontend/pages/Explore.tsx. It showcases a list of product NFTs and recent activities.

Let's break down the key components and how they interact with Aptos:

import { Header } from "@/components/Header"
import ProjectList from "@/components/ProjectList"
import RecentActivity from "@/components/RecentActivity"

export function Explore() {
  return (
    <div className="min-h-screen bg-background">
      <Header title="Explore" />

      <main className="container mx-auto px-4 py-8">
        <div className="flex flex-col md:flex-row gap-8">
          <div className="w-full md:w-2/3">
            <ProjectList />
          </div>
          <div className="w-full md:w-1/3">
            <RecentActivity />
          </div>
        </div>
      </main>
    </div>
  )
}

It serves as a container for two crucial components: ProjectList and RecentActivity.

I imported it to the router in frontend/App.tsx so I can access it during development at http://localhost:5173/explore.

import { Explore } from "./pages/Explore"

// ...

const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      {
        path: "/explore",
        element: <Explore />,
      },
        // other routes

Fetching on-chain events

Let’s see how to fetch and display recent activity by diving into the RecentActivity component in frontend/components/RecentActivity.tsx to understand how to interact with the Aptos blockchain.

import { PRODUCT_NFT_ADDR } from "@/lib/constants"
import { aptosClient } from "@/utils/aptosClient"
import { useQuery } from "@tanstack/react-query"
import { formatDistanceToNow } from "date-fns"
import { User } from "lucide-react"

type UpvoteUpdateEvent = {
  account_address: string
  creation_number: number
  data: {
    upvoter: string
    timestamp: string
    new_upvotes: string
    product_addr: string
    product_name: string
  }
  event_index: number
  sequence_number: number
  transaction_block_height: number
  transaction_version: number
  type: string
  indexed_type: string
}

async function fetchProductEvents(): Promise<UpvoteUpdateEvent[]> {
  const eventType = `${PRODUCT_NFT_ADDR}::product_nft::UpvoteUpdate`

  try {
    const events = await aptosClient().getModuleEventsByEventType({
      eventType: eventType,
    })
    return events as UpvoteUpdateEvent[]
  } catch (error) {
    console.error("Error fetching events:", error)
    return []
  }
}

const RecentActivity: React.FC = () => {
  const {
    data: events,
    isLoading,
    error,
  } = useQuery({
    queryKey: ["productEvents"],
    queryFn: fetchProductEvents,
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error fetching events</div>

  return (
    <div className="p-4 rounded-lg shadow-sm bg-white">
      <h2 className="font-semibold mb-4">Recent Activity</h2>
      <ul className="space-y-4">
        {events?.slice(0, 5).map((event, index) => (
          <li key={index} className="flex items-center space-x-3">
            <div className="rounded-full p-2">
              <User size={16} />
            </div>
            <div>
              <p>
                <span className="font-semibold">
                  {event.data.upvoter.slice(0, 8)}...
                </span>{" "}
                upvoted {event.data.product_name}
              </p>
              <p>
                {formatDistanceToNow(
                  new Date(Number.parseInt(event.data.timestamp) * 1000)
                )}{" "}
                ago
              </p>
            </div>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default RecentActivity

The core of the data fetching lies in the fetchProductEvents function, which consists of the following steps:

  1. I construct the eventType string using the contract address and the specific event I’m interested in. This structure (${address}::${module}::${event}) is how Aptos identifies events.

  2. I use the aptosClient().getModuleEventsByEventType() method to fetch events of this specific type. This is a powerful feature of the Aptos SDK, allowing me to filter events directly on the blockchain.

  3. The events we're fetching have a specific structure defined by the UpvoteUpdateEvent.

This structure corresponds to the data emitted by our smart contract's UpvoteUpdate event. It includes who upvoted, when they upvoted, the new upvote count, and details about the product.

To manage this data fetching in a React component, I’m using React Query:

const {
  data: events,
  isLoading,
  error,
} = useQuery({
  queryKey: ["productEvents"],
  queryFn: fetchProductEvents,
})

This setup allows for efficient caching and refetching of the event data. In the component's render method, I’m displaying the most recent 5 events:

{events?.slice(0, 5).map((event, index) => (
  <li key={index} className="flex items-center space-x-3">
    {/* Event display logic */}
  </li>
))}

Displaying a product list

The ProjectList component in frontend/components/ProjectList.tsx is responsible for fetching and displaying product data.

Let’s explore the components code:

import { useCollectionNFTs } from "@/hooks/useCollectionNFTs"
import { useUpvoteProduct } from "@/hooks/useProductNFT"
import { ChevronUp } from "lucide-react"
import type React from "react"
import { Link } from "react-router-dom"

interface Project {
  id: string
  name: string
  description: string
  image: string
  tags: string[]
  votes: number
}

const ProjectList: React.FC = () => {
  const { loading, error, nftsWithMetadata } = useCollectionNFTs()
  const upvoteProduct = useUpvoteProduct()

  const handleUpvote = (name: string) => {
    upvoteProduct.mutate({
      productName: name,
    })
    console.log(`Upvoted project with id: ${name}`)
  }

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  const projects: Project[] = nftsWithMetadata.map((nft) => ({
    id: nft.token_data_id,
    name: nft.token_name,
    description: nft.description,
    image: nft.image || "/api/placeholder/80/80",
    tags: Object.keys(nft.token_properties).filter(
      (key) => key !== "Upvote Count" && key !== "Product Status"
    ),
    votes: Number.parseInt(nft.token_properties["Upvote Count"] || "0", 10),
  }))

  return (
    <div className="space-y-4">
      {projects.map((project) => (
        <Link key={project.id} to={`/project/${project.id}`} className="block">
          <div className="p-4 rounded-lg shadow-sm flex items-center space-x-4 bg-white">
            <img
              src={project.image}
              alt={project.name}
              className="w-20 h-20 rounded-lg object-cover"
            />
            <div className="flex-grow">
              <h2 className="text-lg font-semibold">{project.name}</h2>
              <p className="text-sm">{project.description}</p>
              <div className="mt-2 space-x-2">
                {project.tags.map((tag) => (
                  <span
                    key={tag}
                    className="inline-block rounded-full px-3 py-1 text-xs font-semibold"
                  >
                    {tag}
                  </span>
                ))}
              </div>
            </div>
            <button
              type="button"
              onClick={(e) => {
                e.preventDefault()
                handleUpvote(project.name)
              }}
              className="flex flex-col items-center justify-center p-2 rounded-md"
            >
              <ChevronUp size={20} />
              <span className="text-sm font-semibold">{project.votes}</span>
            </button>
          </div>
        </Link>
      ))}
    </div>
  )
}

export default ProjectList

This component uses two important hooks: A custom one for fetching advanced data using GraphQL and one for interacting with our smart contract.

Let’s start with looking at the custom useCollectionNFTs hook.

Getting on-chain data with an indexer

Indexers are crucial in blockchain ecosystems because they provide efficient access to historical and aggregated data. They allow us to query complex data structures that are impractical or impossible to fetch directly from the blockchain.

Let's look at how I use an indexer by examining the useCollectionNFTs hook in frontend/hooks/useCollectionNFTs.tsx:

import { COLLECTION_NAME } from "@/lib/constants"
import { GET_COLLECTION_NFTS } from "@/utils/graphql-doc"
import { useQuery } from "@apollo/client"
import { useEffect, useState } from "react"

interface NFT {
  token_name: string
  description: string
  token_uri: string
  collection_id: string
  last_transaction_timestamp: string
  token_data_id: string
  token_properties: {
    "Upvote Count": string
    "Product Status": string
  }
  image?: string
}

interface NFTMetadata {
  name: string
  description: string
  image: string
  external_url: string
}

interface GetCollectionNftsData {
  current_token_datas_v2: NFT[]
}

interface GetCollectionNftsVars {
  collection_name: string
}

export function useCollectionNFTs() {
  const [nftsWithMetadata, setNftsWithMetadata] = useState<NFT[]>([])

  const { loading, error, data } = useQuery<
    GetCollectionNftsData,
    GetCollectionNftsVars
  >(GET_COLLECTION_NFTS, {
    variables: { collection_name: COLLECTION_NAME },
    skip: !COLLECTION_NAME,
  })

  useEffect(() => {
    const fetchNFTMetadata = async (nft: NFT) => {
      try {
        const response = await fetch(nft.token_uri)
        const metadata: NFTMetadata = await response.json()
        return { ...nft, image: metadata.image }
      } catch (error) {
        console.error("Error fetching NFT metadata:", error)
        return nft
      }
    }

    const updateNFTsWithMetadata = async () => {
      if (data?.current_token_datas_v2) {
        const updatedNFTs = await Promise.all(
          data.current_token_datas_v2.map(fetchNFTMetadata)
        )
        setNftsWithMetadata(updatedNFTs)
      }
    }

    updateNFTsWithMetadata()
  }, [data])

  return { loading, error, nftsWithMetadata }
}

This hook encapsulates the logic for fetching NFT data from the collection using an indexer through GraphQL. Here's what's happening:

  1. I use Apollo Client's useQuery hook to execute a GraphQL query (GET_COLLECTION_NFTS) against our indexer.

  2. The query fetches data for all NFTs in our collection, identified by COLLECTION_NAME.

  3. Once I receive the data, I use a useEffect hook to fetch additional metadata for each NFT from its token_uri.

  4. I combine the on-chain data from the indexer with the off-chain metadata to create a complete picture of each NFT.

  5. The hook returns the loading state, potential errors, and the fully populated NFT data.

I created a query with Hasura GraphQL and placed it in the lib/constants.ts file. I built the query on the Hasura website at https://cloud.hasura.io/public/graphiql?endpoint=https://api.testnet.aptoslabs.com/v1/graphql.

import { gql } from "@apollo/client";

/**
 * Edit on Hasura
 * https://cloud.hasura.io/public/graphiql?endpoint=https://api.testnet.aptoslabs.com/v1/graphql
 */
export const GET_COLLECTION_NFTS = gql`
  query GetCollectionNfts($collection_name: String) {
    current_token_datas_v2(
      where: {current_collection: {collection_name: {_eq: $collection_name}}}
      order_by: {last_transaction_timestamp: desc}
    ) {
      token_name
      description
      token_uri
      collection_id
      last_transaction_timestamp
      token_data_id
      token_properties
    }
  }
`

Displaying up-to-date information is only half of the equation. To make the experience truly interactive, users must be able to write data back to the blockchain.

This is where making transactions comes into play. After exploring how to efficiently retrieve on-chain data using indexers and GraphQL, let's learn more about the writing side of blockchain interaction.

Sending transactions

Users should be able to do things on the blockchain, like minting new product NFTs and upvoting existing ones.

To make this happen, I created two main functions:

  1. useMintProductNFT for creating new product NFTs

  2. useUpvoteProduct for upvoting products

I used React Query's useMutation hook for both functions. It seemed like a good fit for handling these blockchain updates. As I worked on this, I encountered a few challenges:

  1. I had to figure out how to send transactions and wait for them to finish.

  2. I needed to show clear error messages when things went wrong.

  3. I had to update the UI after each transaction to show the latest blockchain data.

  4. I wanted to let users know when their actions succeeded or failed.

Let's look at the useUpvoteProduct function I ended up with:

This is my hook in frontend/hooks/useProductNFT.tsx

import { Button } from "@/components/ui/button"
import { ABI } from "@/utils/abi-product_nft"
import { aptosClient } from "@/utils/aptosClient"
import { GET_COLLECTION_NFTS } from "@/utils/graphql-doc"
import { useApolloClient } from "@apollo/client"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useWalletClient } from "@thalalabs/surf/hooks"
import { toast } from "sonner"

export type MintProductNFTArguments = {
  name: string
  description: string
  uri: string
}

export type UpvoteProductArguments = {
  productName: string
}

export const useMintProductNFT = () => {
  const { client } = useWalletClient()
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async ({ name, description, uri }: MintProductNFTArguments) => {
      if (!client) throw new Error("Wallet client not available")
      const result = await client.useABI(ABI).mint_product({
        arguments: [name, description, uri],
        type_arguments: [],
      })
      return result.hash
    },
    onSuccess: async (hash) => {
      const executedTransaction = await aptosClient().waitForTransaction({
        transactionHash: hash,
      })

      queryClient.invalidateQueries()

      const explorerUrl = `https://explorer.aptoslabs.com/txn/${executedTransaction.hash}?network=${process.env.VITE_APP_NETWORK}`

      toast("Success", {
        description: (
          <div>
            <p>Mint transaction succeeded, hash: {executedTransaction.hash}</p>
            <Button
              variant="outline"
              size="sm"
              onClick={() => window.open(explorerUrl, "_blank")}
              className="mt-2"
            >
              View on Explorer
            </Button>
          </div>
        ),
        duration: 5000,
      })

      console.log(`View transaction on explorer: ${explorerUrl}`)
    },
    onError: (error) => {
      console.error(error)
      toast("Error", {
        description: "Failed to mint product NFT",
      })
    },
  })
}

export const useUpvoteProduct = () => {
  const { client } = useWalletClient()
  const queryClient = useQueryClient()
  const apolloClient = useApolloClient()

  return useMutation({
    mutationFn: async ({ productName }: UpvoteProductArguments) => {
      if (!client) throw new Error("Wallet client not available")

      const result = await client.useABI(ABI).upvote_product({
        arguments: [productName],
        type_arguments: [],
      })

      return result.hash
    },
    onSuccess: async (hash) => {
      const executedTransaction = await aptosClient().waitForTransaction({
        transactionHash: hash,
      })

      queryClient.invalidateQueries()
      await apolloClient.refetchQueries({
        include: [GET_COLLECTION_NFTS],
      })

      const explorerUrl = `https://explorer.aptoslabs.com/txn/${executedTransaction.hash}?network=${process.env.VITE_APP_NETWORK}`

      toast.success("Success", {
        description: (
          <div>
            <p>
              Upvote transaction succeeded, hash: {executedTransaction.hash}
            </p>
            <Button
              variant="outline"
              size="sm"
              onClick={() => window.open(explorerUrl, "_blank")}
              className="mt-2"
            >
              View on Explorer
            </Button>
          </div>
        ),
        duration: 5000,
      })

      console.log(`View transaction on explorer: ${explorerUrl}`)
    },
    onError: (error) => {
      console.error(error)
      toast.error("Error", {
        description: "Failed to upvote product",
      })
    },
  })
}

Here's what this function does:

  1. It sends an upvote transaction to the blockchain using client.useABI(ABI).upvote_product(…).

  2. It waits for the transaction to complete using aptosClient().waitForTransaction.

  3. It updates our app's data to reflect the changes with apolloClient.refetchQueries({ include: [GET_COLLECTION_NFTS] }).

  4. It shows a message to let the user know what happened with toast.success("Success"…)

This approach helped me create a smoother user experience. SDKs are the way to go. However, I could also use the Node API or Indexer API to fetch data, which I'll discuss next.

The Node and Indexer API

The Aptos blockchain offers two main APIs for data access: the Node API and the Indexer API. Each had its quirks and strengths, so I had to figure out which would work best for my app.

The Node API

The Node API is a low-level REST interface built right into Aptos full nodes. I found it great for quick, simple queries and real-time data. This was my go-to when submitting transactions or getting the latest blockchain state. It felt fast and straightforward, which I appreciated as I was learning.

Indexer API

The Indexer API is a powerful GraphQL interface that does much of the heavy lifting for you. It processes and indexes blockchain data, making running complex queries or digging into historical data easier. It can handle specialized data like NFTs and token activities.

I recommend starting with the Node API because it is simpler and faster. When you hit its limitations, consider using the more powerful Indexer API.

Challenges

As I built this Product Hunt-style dApp on Aptos, I encountered several challenges:

  1. NFT-based Product Representation: I decided to represent each product as an NFT. This choice came with both benefits and challenges:

    • Benefit: It made querying easier since NFTs follow a standard structure.

    • Challenge: If I had used a custom struct instead, I might have needed to build custom off-chain indexing to fetch the list of items efficiently.

  2. Data Fetching Complexity: Balancing between simple and complex data fetching needs was tricky. I had to use a combination of the SDK for basic interactions and the Indexer API for more intricate queries.

  3. Real-time Updates: Ensuring the UI reflects the latest blockchain state after transactions required careful management of query invalidation and refetching.

Future plans

My next big focus is exploring keyless solutions to improve the user experience. Right now, users have to approve transactions through their web3 wallet every time, which isn't ideal for frequent interactions. I plan to implement authentication using Google social login, which allows interactions without constant approvals.

Conclusion

Looking back on this journey of building our Product Hunt-style dApp on Aptos, I've learned a lot. Each step has been a new challenge, from figuring out how to fetch data using SDKs and indexers to making transactions and handling NFTs. I started with just a basic template and a lot of questions. Now, I have a working app that lets users mint product NFTs, upvote them, and see recent activities.

It could be better, and there's still more to learn, but seeing how far we've come is exciting. Sometimes, the simple Node API is enough, and other times, we need the power of the Indexer API. It's all about finding the right balance for what we're trying to build.

Additional resources

create-aptos-dapp

https://aptos.dev/en/build/create-aptos-dapp

explorer

https://explorer.aptoslabs.com/?network=testnet

Reference

https://aptos.dev/en/build/apis/fullnode-rest-api-reference#tag/accounts

Testnet Playground

https://fullnode.testnet.aptoslabs.com/v1/spec#/

Indexer API Examples

https://aptos.dev/en/build/indexer/get-nft-collections

Indexer API Testnet Playground

https://cloud.hasura.io/public/graphiql?endpoint=https://api.testnet.aptoslabs.com/v1/graphql