Visualizing User Assets From RNS Names With Alchemy

In this guide, we will build a dApp to show the erc20 tokens and NFTS owned by a specific Rootstock wallet address. The address is resolved from the provided Rootstock Name.
RIF Name Service
RIF Name Service (RNS) is a decentralized protocol built on the Rootstock blockchain, similar to Ethereum's ENS. It allows users to register human-readable names (e.g. daniel.rsk) that resolve to Rootstock addresses, making it easier to send and receive assets in a more user-friendly format.
Read more about RNS here.
Prerequisites
Before you begin, make sure you have the following installed:
Node.js (v18 or newer)
Alchemy API Key
Sign up on the Alchemy Dashboard and create an app to get your API key
Setting up your development environment
Create a vite app from the command line by running the command below
yarn create vite
randomjoe@debian:~/Desktop$ yarn create vite
yarn create v1.22.22
(node:9620) [DEP0169] DeprecationWarning: url.parse() behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for url.parse() vulnerabilities.
(Use node --trace-deprecation ... to show where the warning was created)
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-vite@7.0.0" with binaries:
- create-vite
- cva
[##] 2/2│
◇ Project name:
│ rns-visualizer
│
◇ Select a framework:
│ React
│
◇ Select a variant:
│ TypeScript
│
◇ Scaffolding project in /home/mash/Desktop/rns-visualizer...
│
└ Done. Now run:
cd rns-visualizer
yarn
yarn dev
Done in 16.42s.
Install the following dependencies:
axios (fetching data)
yarn add axiosRNS Resolver (convert RNS names to Rootstock addresses for use with Alchemy)
yarn add @rsksmart/rns-resolver.jsshadcn (UI library with many useful components)
yarn add tailwindcss @tailwindcss/viteinitialize shadcn
npx shadcn@latest initadd shadcn components as needed
npx shadcn@latest add button
npx shadcn@latest add tabletanstack query (advanced fetching and caching functionality)
yarn add @tanstack/react-querytanstack table (assets table UI logic)
yarn add @tanstack/react-tablealchemy sdk (interact with Alchemy services)
yarn add alchemy-sdknode polyfill (adds node.js-related functionality not present in the browser, used by the RNS resolver)
yarn add @esbuild-plugins/node-globals-polyfill
After installing the node polyfill, modify yourvite.config.tslike below.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import {nodePolyfills} from 'vite-plugin-node-polyfills'
import path from "path"
import tailwindcss from "@tailwindcss/vite"
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), nodePolyfills(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
}
})
Environment Variables
Create a file .env in the root folder and add in the appropriate variables.
VITE_ALCHEMY_API_KEY='YOUR ALCHEMY API KEY'
VITE_ROOTSTOCK_CHAIN='choose MAINNET or TESTNET'
Create a file env.ts in the src/ folder, which exports type-safe environment variables from the .env to the rest of the app.
export const ALCHEMY_API_KEY = import.meta.env.VITE_ALCHEMY_API_KEY
export const chain = import.meta.env.VITE_ROOTSTOCK_CHAIN
Utility Functions
In the src/ folder, create a file utils.ts. This will contain various useful functions to be imported in other components within the app.
This includes Alchemy and RNS related functions.
import { ALCHEMY_API_KEY, chain } from "./env"
import { Alchemy, Network } from 'alchemy-sdk'
import Resolver from '@rsksmart/rns-resolver.js'
let network
export let resolver: any
export let explorerUrl: string
if (chain === 'TESTNET') {
resolver = Resolver.forRskTestnet({})
network = Network.ROOTSTOCK_TESTNET
explorerUrl = 'https://explorer.testnet.rootstock.io'
} else {
resolver = Resolver.forRskMainnet({})
network = Network.ROOTSTOCK_MAINNET
explorerUrl = 'https://explorer.rootstock.io'
}
export async function resolveRns(name: string): Promise<string | null> {
try {
return await resolver.addr(name)
} catch (error) {
console.error('Error resolving RNS name:', error)
return null
}
}
const settings = {
apiKey: ALCHEMY_API_KEY,
network,
}
export const alchemy = new Alchemy(settings)
export async function getBalances(address: string) {
const balances = await alchemy.core.getTokenBalances(address)
return balances
}
export async function getNfts(address: string) {
const nfts = await alchemy.nft.getNftsForOwner(address)
return nfts
}
export async function getContractMetadata(address: string) {
const metadata = await alchemy.core.getTokenMetadata(address)
return metadata
}
Create the UI
Edit App.tsx and replace it with the following
import './App.css'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Resolver from './components/resolver'
// Initialize tanstack query app-wide
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Resolver />
</QueryClientProvider>
)
}
export default App
The folder src/components should have been automatically created by shadcn, along with another folder ui (ignore that for now, this contains the actual shadcn components).
In the components folder, create the Resolver component in resolver.tsx.
This is the main component in the app. It contains two tables that you can toggle between, the Tokens table and NFTs table, each of which represents the respective asset.
import { Button } from "./ui/button"
import { explorerUrl, resolveRns } from "@/utils"
import { useRef, useState } from "react"
import toast, { Toaster } from "react-hot-toast"
import { TokensTable } from "./tokens-table"
import { NftsTable } from "./nfts-table"
export default function Resolver() {
const [table, setTable] = useState('tokens')
const [input, setInput] = useState('')
const [currentName, setCurrentName] = useState('')
const [resolvedAddress, setResolvedAddress] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [validationError, setValidationError] = useState('');
const inputRef = useRef<HTMLInputElement>(null)
const rnsRegex = /^[a-z0-9]+\.rsk$/;
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setInput(value);
if (value && !rnsRegex.test(value)) {
setValidationError('Name must be lowercase letters/numbers and end with .rsk');
} else {
setValidationError('');
}
}
async function resolve() {
if (!!validationError) return
setIsLoading(true)
setResolvedAddress('')
try {
const address = await resolveRns(input)
if (typeof address !== 'string') {
throw new Error()
}
setResolvedAddress(address as string)
setCurrentName(input)
} catch (error) {
toast.error('Error resolving RNS name')
} finally {
setIsLoading(false)
}
}
return (
<div className="min-h-[100vh] bg-gray-200">
<Toaster position="top-right" />
<div className=''>
<nav className=' flex items-center gap-2 bg-orange-300'>
<img src="/favicon.png" alt="" />
<h2 className='text-2xl font-bold'>RNS Visualizer</h2>
</nav>
<div className="h-[22vh] flex flex-col justify-start pt-10 items-center bg-gray-300">
<div className="">
<input type="search" name="" id=""
value={input}
onChange={handleInputChange}
ref={inputRef}
placeholder='Enter RNS name'
className='p-2 border-2 border-primary bg-white' />
<Button onClick={resolve}
className='ml-2'
disabled={isLoading || !input || !!validationError}>
{!isLoading ? 'RESOLVE' : 'RESOLVING...'}
</Button>
</div>
{validationError && (
<p className="text-red-600 text-sm mt-1">
{validationError}
</p>
)}
{
!isLoading && resolvedAddress &&
<div className="mt-4">
<p className='text-xl'>{currentName}</p>
<p className='font-semibold'>{resolvedAddress}</p>
<a href={`${explorerUrl}/address/${resolvedAddress}`}
className=''
>Explorer Link</a>
<a href=""></a>
</div>
}
</div>
<div className="p-4">
<div className="my-6">
<Button onClick={() => setTable('tokens')}
disabled={!resolvedAddress}
className={`mr-6 ${table === 'tokens' ? 'text-yellow-400' : ''}`}
>Tokens</Button>
<Button onClick={() => setTable('nfts')}
disabled={!resolvedAddress}
className={table !== 'tokens' ? 'text-yellow-400' : 'bg-primary'}
>NFTs</Button>
</div>
{!!resolvedAddress &&
table === 'tokens' ?
<TokensTable address={resolvedAddress.toLowerCase()} />
: <NftsTable address={resolvedAddress.toLowerCase()} />
}
</div>
</div>
</div>
)
}
Inside the same components folder, create three files:
- tokens-table.tsx
A table showing the ERC20 tokens owned by a specific RNS address.
This component contains the logic for fetching token data to be rendered by the data table component, which is passed as rows and columns.
This has two fetch queries, one for fetching the tokens data from the Alchemy token API, the other one for fetching the metadata associated with each contract.
import { useQueries, useQuery } from '@tanstack/react-query';
import { getBalances, getContractMetadata } from '@/utils';
import { useMemo } from 'react';
import { DataTable } from './data-table';
export function TokensTable({ address }: { address: string }) {
if (!address) return null
const {
isLoading: isLoadingBalances,
error: balancesError,
data: balances,
} = useQuery({
queryKey: ['tokens', address],
queryFn: async () => (await getBalances(address)).tokenBalances,
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 60 * 24,
});
const metadataQueries = useQueries({
queries: (balances ?? []).map((token) => ({
queryKey: ['tokenMetadata', token.contractAddress],
queryFn: () => getContractMetadata(token.contractAddress),
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 60 * 24,
enabled: !!balances,
})),
});
const combinedData = useMemo(() => {
if (!balances) {
return []
}
return balances.map((token, index) => {
const metadataResult = metadataQueries[index];
return {
...token,
name: metadataResult.data?.name,
symbol: metadataResult.data?.symbol,
isMetadataLoading: metadataResult.isLoading,
}
})
}, [balances, metadataQueries])
const columns = [
{
header: 'Name',
accessorKey: 'name',
},
{
header: 'Symbol',
accessorKey: 'symbol',
},
{
header: 'Contract',
accessorKey: 'contractAddress',
},
{
header: 'Balance',
accessorKey: 'tokenBalance',
cell: ({ row }: any) => {
const balance = row.original.tokenBalance;
if (!balance) return '0.0';
try {
const value = BigInt(balance);
const decimals = 18n;
const divisor = 10n ** decimals;
const integerPart = value / divisor;
const fractionalPart = value % divisor;
const fractionalStr = fractionalPart.toString().padStart(Number(decimals), '0');
return `${integerPart}.${fractionalStr}`;
} catch (e) {
console.error("Error formatting balance", e);
return "Invalid Balance";
}
},
},
];
if (isLoadingBalances) return <p>Loading Balances...</p>;
if (balancesError) return <p>Error loading tokens</p>;
if (combinedData.length === 0) return <p>No tokens found</p>;
return <DataTable rows={combinedData} columns={columns} />;
}
- nfts-table.tsx
A table showing the ERC721 tokens owned by an RNS address.
This has one fetch query to get data from the Alchemy NFT API along with the associated contract metadata.
Like the tokens table, this passes the data to be rendered in form of rows and columns to the data table component.
import { useQuery } from '@tanstack/react-query';
import { getNfts } from '../utils';
import { DataTable } from './data-table';
export function NftsTable({ address }: { address: string }) {
if (!address) return
const { isLoading, error, data: rows } = useQuery({
queryKey: ['nfts', address],
queryFn: async () => {
return (await getNfts(address)).ownedNfts
},
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 60 * 24,
})
const columns = [
{
header: 'Name',
accessorKey: 'name',
},
{
header: 'Contract',
accessorKey: 'contract.address'
},
{
header: 'Image',
accessorKey: 'image.originalUrl',
},
{
header: 'Amount',
accessorKey: 'balance',
},
{
header: 'Collection',
accessorKey: 'collection',
},
{
header: 'Tx Hash',
accessorKey: 'mint.transactionHash',
},
]
return (
<>
{
isLoading ? <p>Loading...</p>
: error ? <p>Error loading NFTs</p>
: rows?.length === 0 ? <p>No NFTs found</p>
: rows && rows.length > 0 && <DataTable rows={rows} columns={columns} />
}
</>
)
}
- data-table.tsx
A generic wrapper around the shadcn table component.
It takes in rows and columns and renders them into an accessible, responsive table component.
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
export function DataTable({ rows, columns }:any) {
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel()
})
return (
<div className="rounded-md border overflow-x-auto">
<Table>
<TableHeader
className='border-2 border-primary'>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}
>
{headerGroup.headers.map((header, i) => {
return (
<TableHead key={header.id}
className={`
font-bold
sticky
top-0
bg-white
z-20
${i === 0 ? "left-0 z-30 shadow-md" : ""}
`}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row, i) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className={`${i % 2 === 0 ? "bg-gray-200" : "bg-white"}`}
>
{row.getVisibleCells().map((cell, idx) => {
const rendered = flexRender(cell.column.columnDef.cell, cell.getContext());
const value = cell.getValue();
const isFirstColumn = idx === 0;
return (
<TableCell
key={cell.id}
className={`
border-2 border-gray-300
${isFirstColumn ? "sticky left-0 z-10 bg-inherit" : ""}
`}
>
{value ? rendered : "-"}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
Run the app locally
On your terminal, make sure you are in the root folder of the app.
Run the command yarn dev.
This starts the development server on http://localhost:5173 as the default port for vite. Visit that link in your browser to view the app.
Enter a valid RNS name in the input field and press the RESOLVE button. This displays the resolved address and the associated assets.
Here is the GitHub repo for reference.
Conclusion
In this guide, we learned how to resolve RNS names programmatically and how to use Alchemy within the Rootstock ecosystem to retrieve asset data.
For more detailed information about the Rootstock ecosystem, visit the developer docs.





