Sample dApp RNS Integration
This guide walks you through building a React dApp that integrates with the Rootstock Name Service (RNS) using ethers.js. You'll create a functional application that allows users to resolve domain names to addresses, check domain availability, perform reverse lookups, and more etc.
By the end of this guide, you'll have a dApp that demonstrates core RNS operations and serves as a foundation for building more complex blockchain applications on Rootstock.
Prerequisites
Before starting, ensure you have the following:
- Node.js and npm installed
- A wallet preferably MetaMask configured for Rootstock Mainnet or Testnet
- Basic understanding of Solidity and Web3.js or ethers.js
- Rootstock Testnet Faucet
All the code snippets shown in this guide will be added inside your App.js file.
1. Project Setup
Before you begin, ensure you have a React app set up. If not, create one with:
npx create-react-app rns-dapp
cd rns-dapp
Then install the required dependencies:
npm install ethers @ethersproject/providers @ethersproject/contracts @ethersproject/constants @ethersproject/hash react-dom react-scripts @rsksmart/rns
2. Change Index.js to React Dom
Go to index.js and change it to the following code:
import App from './App';
import './index.css';
ReactDOM.render(
<App />,
document.getElementById('root')
);
``
### 3. Import the Required Modules
Open your `App.js` file and import the following modules:
```js
import React, { useState, useCallback, useMemo } from "react";
import { JsonRpcProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";
import { namehash } from "@ethersproject/hash";
4. Initialize RNS
Next, set up RNS using the RNS Registry Contract Address and the RSK Testnet RPC URL.
Add the following code inside App.js:
// Rootstock Testnet RPC
const ROOTSTOCK_RPC_NODE = "https://public-node.testnet.rsk.co";
// RNS registry (testnet)
const RNS_REGISTRY_ADDRESS = "0x7d284aaac6e925aad802a53c0c69efe3764597b8";
// Bitcoin chain ID for multichain resolver
const BITCOIN_CHAIN_ID = 0;
// ABIs
const RNS_REGISTRY_ABI = [
"function resolver(bytes32 node) view returns (address)",
"function owner(bytes32 node) view returns (address)",
];
const RNS_ADDR_RESOLVER_ABI = [
"function addr(bytes32 node) view returns (address)",
"function addr(bytes32 node, uint coinType) view returns (bytes)",
];
const RNS_NAME_RESOLVER_ABI = [
"function name(bytes32 node) view returns (string)",
];
const provider = new JsonRpcProvider(ROOTSTOCK_RPC_NODE);
const registry = new Contract(
RNS_REGISTRY_ADDRESS,
RNS_REGISTRY_ABI,
provider
);
What this does:
- Connects to the Rootstock Testnet
- Initializes the RNS Registry contract
- Sets up the provider for contract interactions
5. Utility Functions
Add a utility function to strip the hex prefix from addresses:
const stripHexPrefix = (hex: string): string => hex.slice(2);
6. Style for the UI
Add the following style for the UI:
const styles = {
container: {
minHeight: "100vh",
backgroundColor: "#f5f6fa",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "20px",
fontFamily: "Arial, sans-serif",
},
card: {
backgroundColor: "#fff",
padding: "25px",
borderRadius: "10px",
maxWidth: "520px",
width: "100%",
boxShadow: "0 2px 6px rgba(0,0,0,0.1)",
},
heading: {
textAlign: "center",
marginBottom: "20px",
fontSize: "22px",
},
input: {
padding: "10px",
width: "100%",
borderRadius: "6px",
border: "1px solid #ccc",
marginBottom: "10px",
boxSizing: "border-box",
},
buttonGroup: {
display: "flex",
gap: "10px",
marginBottom: "10px",
},
button: {
flex: 1,
padding: "10px",
backgroundColor: "#388e3c",
color: "#fff",
border: "none",
borderRadius: "6px",
cursor: "pointer",
},
buttonAlt: {
flex: 1,
padding: "10px",
backgroundColor: "#1976d2",
color: "#fff",
border: "none",
borderRadius: "6px",
},
buttonWide: {
width: "100%",
padding: "10px",
backgroundColor: "#5e35b1",
color: "#fff",
border: "none",
borderRadius: "6px",
margin: "10px",
marginBottom: "5px",
},
ownerBox: {
padding: "10px",
backgroundColor: "#e8f5e9",
borderRadius: "6px",
border: "1px solid #c8e6c9",
marginTop: "10px",
},
divider: {
margin: "20px 0",
borderTop: "1px solid #ddd",
},
resultBox: {
marginTop: "10px",
padding: "10px",
backgroundColor: "#f9f9f9",
border: "1px solid #ddd",
borderRadius: "6px",
},
};
7. Core RNS Functions
Add the core functions that handle different RNS operations.
a. Resolve a Rootstock Address
Looks up the RSK address linked to a given RNS domain:
const resolveRnsName = async (name) => {
try {
const nameHash = namehash(name);
const resolverAddress = await registry.resolver(nameHash);
if (resolverAddress === AddressZero) {
return null;
}
const addrResolverContract = new Contract(
resolverAddress,
RNS_ADDR_RESOLVER_ABI,
provider
);
// Use the functions property to call overloaded functions
const [address] = await addrResolverContract.functions["addr(bytes32)"](
nameHash
);
if (!address || address === AddressZero) {
return null;
}
return address.toLowerCase();
} catch (e) {
console.error("resolveRnsName error", e);
return null;
}
};
When working with overloaded contract functions in ethers.js, you must use the functions property with the full function signature (e.g., functions["addr(bytes32)"]). This is necessary because the RNS resolver contract has multiple addr functions with different parameters.
b. Resolve a Bitcoin Address
Fetches the Bitcoin address if the domain has one:
const resolveBitcoinAddress = async (name) => {
try {
const hash = namehash(name);
const resolver = await registry.resolver(hash);
if (resolver === AddressZero) return null;
const resolverContract = new Contract(
resolver,
RNS_ADDR_RESOLVER_ABI,
provider
);
const [btcBytes] = await resolverContract.functions[
"addr(bytes32,uint256)"
](hash, BITCOIN_CHAIN_ID);
if (!btcBytes || btcBytes === "0x") return null;
return btcBytes;
} catch (e) {
console.error("resolveBitcoinAddress error", e);
return null;
}
};
c. Reverse Lookup (Address to Name)
Looks up the RNS name associated with an address:
const lookupAddress = async (address) => {
try {
const reverseHash = namehash(`${stripHexPrefix(address)}.addr.reverse`);
const resolver = await registry.resolver(reverseHash);
if (resolver === AddressZero) return null;
const resolverContract = new Contract(
resolver,
RNS_NAME_RESOLVER_ABI,
provider
);
const name = await resolverContract.name(reverseHash);
return name || null;
} catch (e) {
console.error("lookupAddress error", e);
return null;
}
};
d. Get Domain Owner
Retrieves the owner address of a domain:
const getDomainOwner = async (domain) => {
try {
const hash = namehash(domain);
const owner = await registry.owner(hash);
if (!owner || owner === AddressZero) return null;
return owner.toLowerCase();
} catch (e) {
console.error("owner error", e);
return null;
}
}
;
e. Check Domain Availability
Checks if a domain is available for registration:
const checkDomainAvailability = async (domain) => {
try {
const hash = namehash(domain);
const owner = await registry.owner(hash);
return owner === AddressZero;
} catch (e) {
console.error("availability error", e);
return false;
}
};
f. Check Subdomain Availability
Checks if a subdomain is available:
const checkSubdomainAvailability = async (
domain,
subdomain
)=> {
return checkDomainAvailability(`${subdomain}.${domain}`);
};
6. Custom RNS Hook
Create a custom hook to encapsulate all RNS functionality:
const useRns = () => {
const getAddressByRns = useCallback(async (name) => {
return await resolveRnsName(name);
}, []);
const getBitcoinAddressByRns = useCallback(async (name) => {
return await resolveBitcoinAddress(name);
}, []);
const getRnsName = useCallback(async (address) => {
return await lookupAddress(address);
}, []);
const getOwner = useCallback(async (domain) => {
return await getDomainOwner(domain);
}, []);
const checkAvailability = useCallback(async (domain) => {
return await checkDomainAvailability(domain);
}, []);
const checkSubdomain = useCallback(
async (domain: string, subdomain: string) => {
return await checkSubdomainAvailability(domain, subdomain);
},
[]
);
return useMemo(
() => ({
getAddressByRns,
getBitcoinAddressByRns,
getRnsName,
getOwner,
checkAvailability,
checkSubdomain,
}),
[
getAddressByRns,
getBitcoinAddressByRns,
getRnsName,
getOwner,
checkAvailability,
checkSubdomain,
]
);
};
8. Main Component
Create the main component with UI and event handlers inside the same App.js:
export default function App() {
const {
getAddressByRns,
getBitcoinAddressByRns,
getRnsName,
getOwner,
checkAvailability,
checkSubdomain,
} = useRns();
const [domain, setDomain] = useState("");
const [subdomain, setSubdomain] = useState("");
const [address, setAddress] = useState("");
const [result, setResult] = useState("");
const [owner, setOwner] = useState("");
const [loading, setLoading] = useState(false);
const wrap = async (fn) => {
setLoading(true);
setResult("");
setOwner("");
try {
await fn();
} finally {
setLoading(false);
}
};
return (
<div style={styles.container}>
<div style={styles.card}>
<h1 style={styles.heading}>Rootstock Name Service Lookup</h1>
<input
style={styles.input}
value={domain}
onChange={(e) => setDomain(e.target.value)}
placeholder="Enter domain like testing.rsk"
/>
<div style={styles.buttonGroup}>
<button
style={styles.button}
disabled={loading}
onClick={() =>
wrap(async () => {
const addr = await getAddressByRns(domain);
setResult(addr || "No RSK address found");
})
}
>
Resolve RSK
</button>
<button
style={styles.buttonAlt}
disabled={loading}
onClick={() =>
wrap(async () => {
const btc = await getBitcoinAddressByRns(domain);
setResult(btc || "No Bitcoin address found");
})
}
>
Resolve BTC
</button>
</div>
<button
style={styles.buttonWide}
disabled={loading}
onClick={() =>
wrap(async () => {
const available = await checkAvailability(domain);
setResult(
available ? "✅ Domain is available" : "❌ Domain is taken"
);
})
}
>
Check Availability
</button>
<div style={{ display: "flex", gap: "8px" }}>
<input
style={{ ...styles.input, flex: 1 }}
value={subdomain}
onChange={(e) => setSubdomain(e.target.value)}
placeholder="Enter subdomain"
/>
<button
style={styles.buttonAlt}
disabled={loading}
onClick={() =>
wrap(async () => {
const available = await checkSubdomain(domain, subdomain);
setResult(
available
? "✅ Subdomain is available"
: "❌ Subdomain is taken"
);
})
}
>
Check Subdomain
</button>
</div>
<button
style={styles.buttonWide}
disabled={loading}
onClick={() =>
wrap(async () => {
const ownerAddr = await getOwner(domain);
if (ownerAddr) {
setOwner(ownerAddr);
setResult("Owner found");
} else {
setResult("No owner found");
}
})
}
>
Get Owner
</button>
{owner && (
<div style={styles.ownerBox}>
<strong>Owner:</strong> {owner}
</div>
)}
<hr style={styles.divider} />
<input
style={styles.input}
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="Reverse lookup (address)"
/>
<button
style={styles.buttonWide}
disabled={loading}
onClick={() =>
wrap(async () => {
const name = await getRnsName(address);
setResult(name || "No reverse entry found");
})
}
>
Reverse Lookup
</button>
<div style={styles.resultBox}>{loading ? "Loading..." : result}</div>
</div>
</div>
);
}
Full App.js code
import React, { useState, useCallback, useMemo } from "react";
import { JsonRpcProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";
import { namehash } from "@ethersproject/hash";
const ROOTSTOCK_RPC_NODE = "https://public-node.testnet.rsk.co";
// RNS registry (testnet)
const RNS_REGISTRY_ADDRESS = "0x7d284aaac6e925aad802a53c0c69efe3764597b8";
// Bitcoin chain ID for multichain resolver
const BITCOIN_CHAIN_ID = 0;
// ABIs
const RNS_REGISTRY_ABI = [
"function resolver(bytes32 node) view returns (address)",
"function owner(bytes32 node) view returns (address)",
];
const RNS_ADDR_RESOLVER_ABI = [
"function addr(bytes32 node) view returns (address)",
"function addr(bytes32 node, uint coinType) view returns (bytes)",
];
const RNS_NAME_RESOLVER_ABI = [
"function name(bytes32 node) view returns (string)",
];
const provider = new JsonRpcProvider(ROOTSTOCK_RPC_NODE);
const registry = new Contract(RNS_REGISTRY_ADDRESS, RNS_REGISTRY_ABI, provider);
const stripHexPrefix = (hex) => hex.slice(2);
const styles = {
container: {
minHeight: "100vh",
backgroundColor: "#f5f6fa",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "20px",
fontFamily: "Arial, sans-serif",
},
card: {
backgroundColor: "#fff",
padding: "25px",
borderRadius: "10px",
maxWidth: "520px",
width: "100%",
boxShadow: "0 2px 6px rgba(0,0,0,0.1)",
},
heading: {
textAlign: "center",
marginBottom: "20px",
fontSize: "22px",
},
input: {
padding: "10px",
width: "100%",
borderRadius: "6px",
border: "1px solid #ccc",
marginBottom: "10px",
boxSizing: "border-box",
},
buttonGroup: {
display: "flex",
gap: "10px",
marginBottom: "10px",
},
button: {
flex: 1,
padding: "10px",
backgroundColor: "#388e3c",
color: "#fff",
border: "none",
borderRadius: "6px",
cursor: "pointer",
},
buttonAlt: {
flex: 1,
padding: "10px",
backgroundColor: "#1976d2",
color: "#fff",
border: "none",
borderRadius: "6px",
},
buttonWide: {
width: "100%",
padding: "10px",
backgroundColor: "#5e35b1",
color: "#fff",
border: "none",
borderRadius: "6px",
margin: "10px",
marginBottom: "5px",
},
ownerBox: {
padding: "10px",
backgroundColor: "#e8f5e9",
borderRadius: "6px",
border: "1px solid #c8e6c9",
marginTop: "10px",
},
divider: {
margin: "20px 0",
borderTop: "1px solid #ddd",
},
resultBox: {
marginTop: "10px",
padding: "10px",
backgroundColor: "#f9f9f9",
border: "1px solid #ddd",
borderRadius: "6px",
},
};
// Resolve RSK address
const resolveRnsName = async (name) => {
try {
const nameHash = namehash(name);
const resolverAddress = await registry.resolver(nameHash);
if (resolverAddress === AddressZero) {
return null;
}
const addrResolverContract = new Contract(
resolverAddress,
RNS_ADDR_RESOLVER_ABI,
provider
);
// Use the functions property to call overloaded functions
const [address] = await addrResolverContract.functions["addr(bytes32)"](
nameHash
);
if (!address || address === AddressZero) {
return null;
}
return address.toLowerCase();
} catch (e) {
console.error("resolveRnsName error", e);
return null;
}
};
// Resolve BTC address
const resolveBitcoinAddress = async (name) => {
try {
const hash = namehash(name);
const resolver = await registry.resolver(hash);
if (resolver === AddressZero) return null;
const resolverContract = new Contract(
resolver,
RNS_ADDR_RESOLVER_ABI,
provider
);
const [btcBytes] = await resolverContract.functions[
"addr(bytes32,uint256)"
](hash, BITCOIN_CHAIN_ID);
if (!btcBytes || btcBytes === "0x") return null;
return btcBytes;
} catch (e) {
console.error("resolveBitcoinAddress error", e);
return null;
}
};
// Reverse lookup (address to name)
const lookupAddress = async (address) => {
try {
const reverseHash = namehash(`${stripHexPrefix(address)}.addr.reverse`);
const resolver = await registry.resolver(reverseHash);
if (resolver === AddressZero) return null;
const resolverContract = new Contract(
resolver,
RNS_NAME_RESOLVER_ABI,
provider
);
const name = await resolverContract.name(reverseHash);
return name || null;
} catch (e) {
console.error("lookupAddress error", e);
return null;
}
};
// Get owner
const getDomainOwner = async (domain) => {
try {
const hash = namehash(domain);
const owner = await registry.owner(hash);
if (!owner || owner === AddressZero) return null;
return owner.toLowerCase();
} catch (e) {
console.error("owner error", e);
return null;
}
};
// Domain availability
const checkDomainAvailability = async (domain) => {
try {
const hash = namehash(domain);
const owner = await registry.owner(hash);
return owner === AddressZero;
} catch (e) {
console.error("availability error", e);
return false;
}
};
// Subdomain availability
const checkSubdomainAvailability = async (
domain,
subdomain
) => {
return checkDomainAvailability(`${subdomain}.${domain}`);
};
// RNS Hook
const useRns = () => {
const getAddressByRns = useCallback(async (name) => {
return await resolveRnsName(name);
}, []);
const getBitcoinAddressByRns = useCallback(async (name) => {
return await resolveBitcoinAddress(name);
}, []);
const getRnsName = useCallback(async (address) => {
return await lookupAddress(address);
}, []);
const getOwner = useCallback(async (domain) => {
return await getDomainOwner(domain);
}, []);
const checkAvailability = useCallback(async (domain) => {
return await checkDomainAvailability(domain);
}, []);
const checkSubdomain = useCallback(
async (domain, subdomain) => {
return await checkSubdomainAvailability(domain, subdomain);
},
[]
);
return useMemo(
() => ({
getAddressByRns,
getBitcoinAddressByRns,
getRnsName,
getOwner,
checkAvailability,
checkSubdomain,
}),
[
getAddressByRns,
getBitcoinAddressByRns,
getRnsName,
getOwner,
checkAvailability,
checkSubdomain,
]
);
};
// UI Component
export default function App() {
const {
getAddressByRns,
getBitcoinAddressByRns,
getRnsName,
getOwner,
checkAvailability,
checkSubdomain,
} = useRns();
const [domain, setDomain] = useState("");
const [subdomain, setSubdomain] = useState("");
const [address, setAddress] = useState("");
const [result, setResult] = useState("");
const [owner, setOwner] = useState("");
const [loading, setLoading] = useState(false);
const wrap = async (fn) => {
setLoading(true);
setResult("");
setOwner("");
try {
await fn();
} finally {
setLoading(false);
}
};
return (
<div style={styles.container}>
<div style={styles.card}>
<h1 style={styles.heading}>Rootstock Name Service Lookup</h1>
<input
style={styles.input}
value={domain}
onChange={(e) => setDomain(e.target.value)}
placeholder="Enter domain like testing.rsk"
/>
<div style={styles.buttonGroup}>
<button
style={styles.button}
disabled={loading}
onClick={() =>
wrap(async () => {
const addr = await getAddressByRns(domain);
setResult(addr || "No RSK address found");
})
}
>
Resolve RSK
</button>
<button
style={styles.buttonAlt}
disabled={loading}
onClick={() =>
wrap(async () => {
const btc = await getBitcoinAddressByRns(domain);
setResult(btc || "No Bitcoin address found");
})
}
>
Resolve BTC
</button>
</div>
<button
style={styles.buttonWide}
disabled={loading}
onClick={() =>
wrap(async () => {
const available = await checkAvailability(domain);
setResult(
available ? "✅ Domain is available" : "❌ Domain is taken"
);
})
}
>
Check Availability
</button>
<div style={{ display: "flex", gap: "8px" }}>
<input
style={{ ...styles.input, flex: 1 }}
value={subdomain}
onChange={(e) => setSubdomain(e.target.value)}
placeholder="Enter subdomain"
/>
<button
style={styles.buttonAlt}
disabled={loading}
onClick={() =>
wrap(async () => {
const available = await checkSubdomain(domain, subdomain);
setResult(
available
? "✅ Subdomain is available"
: "❌ Subdomain is taken"
);
})
}
>
Check Subdomain
</button>
</div>
<button
style={styles.buttonWide}
disabled={loading}
onClick={() =>
wrap(async () => {
const ownerAddr = await getOwner(domain);
if (ownerAddr) {
setOwner(ownerAddr);
setResult("Owner found");
} else {
setResult("No owner found");
}
})
}
>
Get Owner
</button>
{owner && (
<div style={styles.ownerBox}>
<strong>Owner:</strong> {owner}
</div>
)}
<hr style={styles.divider} />
<input
style={styles.input}
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="Reverse lookup (address)"
/>
<button
style={styles.buttonWide}
disabled={loading}
onClick={() =>
wrap(async () => {
const name = await getRnsName(address);
setResult(name || "No reverse entry found");
})
}
>
Reverse Lookup
</button>
<div style={styles.resultBox}>{loading ? "Loading..." : result}</div>
</div>
</div>
);
}
10. Start Local Development Server
To preview this application, open your terminal and run the following command:
npm run start
This is how your UI should look:

It should also function properly, as shown in the demo below:
Conclusion
In this guide, you've learned how to integrate RNS into your React app using ethers.js. You now understand how to:
- Set up RNS with ethers.js providers and contracts
- Handle overloaded contract functions properly
- Create reusable functions for RNS operations
- Build a custom React hook for RNS functionality
- Implement a complete UI for RNS lookups and checks
This pattern can be easily extended or modified for your specific use case.