본문으로 바로가기
Time to read: 1 min

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;
}
};

Important Note on Overloaded Functions

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:

RNS dApp

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:

  1. Set up RNS with ethers.js providers and contracts
  2. Handle overloaded contract functions properly
  3. Create reusable functions for RNS operations
  4. Build a custom React hook for RNS functionality
  5. Implement a complete UI for RNS lookups and checks

This pattern can be easily extended or modified for your specific use case.

Resources

최종 수정: 작성일: 작성자: Ileolami