This guide provides an initial overview of using the Unlockd SDK for borrowing against RWAs functionalities. As we're in the early stages of documentation, your feedback is crucial. We encourage developers to explore this use case and help shape these materials.
We're committed to supporting your integration journey. If you need personalized assistance or have suggestions, please reach out to us directly. Your input will help us create more comprehensive and useful documentation for the developer community.
Overview
Borrowing against Real-World Assets (RWAs) is a powerful feature of the Unlockd protocol. By leveraging the Unlockd SDK, developers can enable users to collateralize their tokenized assets and obtain instant loans.
This feature can be applied to loans with a single asset or even a portfolio of assets from different classes.
If your product allows users to access an inventory or portfolio, you can natively display their Available To Borrow amount and allow them to bundle all items into a single or different loans without leaving your interface
How-To Guide
This guide will walk you through the process of implementing a borrowing feature using the Unlockd SDK, allowing users to borrow against RWAs.
Prerequisites
Before you begin, ensure you have the following set up:
A React-based project
The Unlockd SDK installed (@verislabs/unlockd-sdk)
Basic understanding of React hooks and Ethereum transactions
This example is written in TypeScript and uses the wagmi library along with its dependencies. However, it's important to note that wagmi is not required to use the Unlockd SDK. The SDK itself is only dependent on viem.
Developers are free to use their preferred tools and libraries for wallet connection and blockchain interactions. This example serves as one possible implementation approach.
Implementation Steps
Set up the React component with necessary state variables and hooks.
Implement login, connection and authentication
Fetch NFTs and prices from connected wallets.
Create or refresh Unlockd wallet.
Implement NFT selection functionality.
Implement NFT approval and transfer to Unlockd wallet.
Set up borrowing functionality against selected NFTs.
Render the user interface with all interactive elements.
Code Overview
1. Set up the React component
In your component, first set up all required state variables (using useState, Context, or your own state manager) and wagmi hooks. This includes states for managing user accounts, NFT selections, approval statuses, and other essential data. For the Unlockd SDK you only need to initialize the api object.
"use client";import { useState, useEffect } from"react";import { useAccount, useConnect, useDisconnect, useConfig, useWriteContract, useReadContracts,} from"wagmi";import { signMessage, readContract, writeContract, waitForTransactionReceipt, multicall, getBalance,} from"wagmi/actions";import { sepolia } from"wagmi/chains";import { injected } from"wagmi/connectors";import { parseUnits, Address, isAddress } from"viem";import { UnlockdApi, Chains, UnderlyingsAsset, borrow, underlyingsAssets, createWallet, getWallet,} from"@verislabs/unlockd-sdk";import { nftAbi } from"../../abis/ERC721Abi";import { nftBatchTransferAbi } from"../../abis/NftBatchTransferAbi";interfaceNFTPriceData { collection:Address; tokenId:string; underlyingAsset:Address; price?:string;}constALLOWED_COLLECTIONS:Address[] = ["0xa6a9acfdd1f64ec324ee936344cdb1457bdbddf0","0x388043e55a388e07a75e9a1412fe2d64e48343a5",];constnftBatchTransferAddress="0xaba905eba39b9a55fd0f910a6415ba91c3e9353d"asAddress;// The idea is to create a simple example of how to use the Unlockd SDK to borrow against NFTs.// The user can connect their wallet, log in to Unlockd, approve a collection, select NFTs to transfer to their Unlockd wallet, and borrow against them.
// We are not demonstrating frontend code, or handling states, just the logic to interact with the Unlockd SDK and create a borrow transaction.
functionApp() {// VariablesconstuserAccount=useAccount();const { connect,connectors } =useConnect();const { disconnect } =useDisconnect();constconfig=useConfig();const [unlockdAccount,setUnlockdAccount] =useState<Address>();const [token,setToken] =useState<string|null>(null);const [selectedNFTs,setSelectedNFTs] =useState<NFTPriceData[]>([]);const [selectedUnderlyingAsset,setSelectedUnderlyingAsset] =useState<Address>(underlyingsAssets(Chains.Sepolia)[UnderlyingsAsset.USDC]);const [approvalStatus,setApprovalStatus] = useState< Record<Address,"not-approved"|"approved"|"pending">>({});const [injectedWalletNFTs,setInjectedWalletNFTs] =useState<NFTPriceData[]>( [], );const [unlockdWalletNFTs,setUnlockdWalletNFTs] =useState<NFTPriceData[]>( [], );const [selectedUnlockdNFTs,setSelectedUnlockdNFTs] = useState< NFTPriceData[]>([]);const [borrowAmount,setBorrowAmount] =useState<string>("");constapi=newUnlockdApi(Chains.Sepolia);
In this example, all the code is contained within the root component (App) for simplicity. However, this functionality can be implemented in any React component.
The hooks used (useAccount, useDisconnect, etc.) are imported from wagmi, while useState is from React. These reactive variables can be stored and managed using any state management solution you prefer.
It's important to note the initialization of the Unlockd SDK, which is a crucial step:
constapi=newUnlockdApi(Chains.Sepolia);
This line, found at the end of the component, sets up the Unlockd API instance for interacting with the Sepolia testnet. Make sure to initialize the SDK appropriately in your implementation.
2. Implement login, connection and authentication
Set up functionality authenticating with Unlockd. This step calls the Unlockd SDK for obtaining an authentication token for the user's currently connected address by signing a blockchain message. The auth token is essential for user's interaction with the Unlockd SDK
// The login should call the unlockd API using the signatureMessage method to get the message to sign.// We get the message to sign, and then we trigger it to the user so he can sign it (signMessage). // After the user signs the message, we validate it using the validateMessage method, and returning the auth token we will need to interact with the protocol.
constlogIn=async () => {if (!userAccount.address) {return; }try {constmessage=awaitapi.signatureMessage(userAccount.address);constsignature=awaitsignMessage(config, { account:userAccount.address, message:message.message, });constauthToken=awaitapi.validateMessage(userAccount.address, signature, );setToken(authToken.token); } catch (error) {console.error("Login failed:", error); } };
3. Fetch and display NFTs from connected wallets.
Implement logic to fetch NFTs (RWAs) from both the user's connected wallet and their Unlockd wallet, if the wallet was created before (see #4 below).
This involves querying blockchain data for NFT balances and token IDs, then passing this data to the Unlockd API to retrive price data for all NFTs. In this example, we are quering the blockchain directly to obtain the NFTs data (using the multicall action from wagmi for efficiency) but you can obtain this data from any means (such as using the Alchemy SDK).
What you need to be aware of is the data structure expected by the Unlockd API when calling the prices method, as see in the priceParams.
// In order to borrow, we 1st should check if the user has any Allowed Collections in his wallet. If yes: we shouldt get the balance of it, and then get the token ID of each NFT in order to provide the price. If no: it ends here.
// The fetchNftsAndPrices should be called from the injected wallet and the unlockd wallet, since both will hold assets.
constfetchNFTsAndPrices=async ( walletAddress:Address, selectedUnderlyingAsset:Address, setNFTs:React.Dispatch<React.SetStateAction<NFTPriceData[]>>, ) => {if (!walletAddress ||!isAddress(walletAddress)) {console.error(`Invalid wallet address: ${walletAddress}`);return; }if (!selectedUnderlyingAsset ||!isAddress(selectedUnderlyingAsset)) {console.error(`Invalid selected underlying asset: ${selectedUnderlyingAsset}`, );return; }try {constbalanceContracts=ALLOWED_COLLECTIONS.map((collection) => ({ address: collection, abi:nftAbi.abi, functionName:"balanceOf", args: [walletAddress], } asconst));constbalances=awaitmulticall(config, { contracts: balanceContracts });consttokenIdContracts=balances.flatMap((balance, index) => {if (balance.status !=='success') return [];constnumTokens=Number(balance.result ||0);if (numTokens ===0) return [];returnArray.from({ length: numTokens }, (_, i) => ({ address:ALLOWED_COLLECTIONS[index], abi:nftAbi.abi, functionName:"tokenOfOwnerByIndex", args: [walletAddress,BigInt(i)], } asconst)); });consttokenIds=awaitmulticall(config, { contracts: tokenIdContracts });constpriceParams= tokenIds.filter((tokenIdResult) =>tokenIdResult.status ==='success').map((tokenIdResult, index) => ({ collection: tokenIdContracts[index].address, tokenId:tokenIdResult.result.toString(), underlyingAsset: selectedUnderlyingAsset, }));constpricesResponse=awaitapi.prices(priceParams);constnftsPriceData=pricesResponse.map((result) => ({ collection:result.collection asAddress, tokenId:result.tokenId, underlyingAsset: selectedUnderlyingAsset, price:result.valuation, }));setNFTs(nftsPriceData); } catch (error) {console.error(`Error fetching NFTs and prices for wallet ${walletAddress}:`, error, ); } };
For each NFT, you need to create an object with the following structure:
{ collection: string,// NFT's collection address tokenId: string,// NFT's ERC721 token id underlyingAsset: string // the asset ETH or USDC you want to receive the price evaluation in }
Form an array with similar objects containing data about all the NFTs you want to evaluate, and pass it to the prices() method.
4. Create or retrieve Unlockd wallet
Set up functionality to create a new Unlockd wallet (Unlockd Account) if the user doesn't have one, or refresh an existing one. This step is crucial for storing the assets that will be used as collateral for borrowing.
// In order to create a loan, you will need to own an unlockd wallet.// The assets (nfts) will be locked in your unlockd wallet.// If you don't have an unlockd wallet, it will be created.constcreateOrRefreshUnlockdWallet=async () => {try {let unlockdWallet =awaitgetWallet({ network:Chains.Sepolia });if (!unlockdWallet) {awaitcreateWallet({ network:Chains.Sepolia }); unlockdWallet =awaitgetWallet({ network:Chains.Sepolia }); }if (unlockdWallet) {setUnlockdAccount(unlockdWallet);awaitfetchNFTsAndPrices(unlockdWallet, selectedUnderlyingAsset, setUnlockdWalletNFTs); } } catch (error) {console.error("Error with Unlockd wallet:", error); } };
5. Implement NFT selection functionality
Create a mechanism for users to select one or multiple NFTs, either for transferring to the Unlockd wallet or for using as collateral when borrowing. This function is reused in selecting NFTs in both the user's wallet and the Unlockd wallet.
// Allows the user to select 1 or multiple NFTs to transfer to the unlockd wallet or to borrow agains them, if used from the unlockd wallet.
consttoggleNFTSelection= (nft:NFTPriceData, isUnlockdWallet:boolean) => {constsetNFTs= isUnlockdWallet ? setSelectedUnlockdNFTs : setSelectedNFTs;setNFTs((prev) =>prev.some( (item) =>item.collection ===nft.collection &&item.tokenId ===nft.tokenId, )?prev.filter( (item) =>!(item.collection ===nft.collection &&item.tokenId ===nft.tokenId ), ): [...prev, nft], ); };
6. Implement NFT approval and transfer to Unlockd wallet
Set up the process for approving NFT collections and transferring selected NFTs to the Unlockd wallet. This includes checking the current approval status for all selected NFTs, sending approval transactions for each collection, and transferring the selected NFTs in batch to the Unlockd wallet.
// This will validate if the user has approved the BatchNFTTransfer contract to transfer the NFTs from his injected wallet to the unlockd wallet.
// If the approvals exist you dont need to approve again. If not, the user SHOULD APPROVE.// The SDK should provide a function to approve!consthandleApprove=async (collectionAddress:Address) => {if (!userAccount.address) {return; }try {constisApproved=awaitreadContract(config, { address: collectionAddress, abi:nftAbi.abi, functionName:"isApprovedForAll", args: [userAccount.address, nftBatchTransferAddress], });if (isApproved) {setApprovalStatus((prev) => ({...prev, [collectionAddress]:"approved", }));return; }constresponse=awaitwriteContract(config, { address: collectionAddress, abi:nftAbi.abi, functionName:"setApprovalForAll", args: [nftBatchTransferAddress,true], });setApprovalStatus((prev) => ({...prev, [collectionAddress]:"pending", }));consttransactionReceipt=awaitwaitForTransactionReceipt(config, { hash: response, });if (transactionReceipt.status ==="success") {setApprovalStatus((prev) => ({...prev, [collectionAddress]:"approved", })); } else {setApprovalStatus((prev) => ({...prev, [collectionAddress]:"not-approved", })); } } catch (error) {console.error("Approval failed:", error);setApprovalStatus((prev) => ({...prev, [collectionAddress]:"not-approved", })); } };const { writeContract: batchTransfer } =useWriteContract();// This function will transfer the selected NFTs from the injected wallet to the unlockd wallet.// The approve of this contract and its usage is not mandatory.// The important is the the unlockd wallet has the NFTs. // This contract was developed and audited by us, so we use it, feel free to use it, or change to other preferred method.
consthandleBatchTransfer=async () => {if (!userAccount.address ||!unlockdAccount ||selectedNFTs.length===0)return;try {constnftTransfers=selectedNFTs.map((nft) => ({ contractAddress:nft.collection, tokenId:BigInt(nft.tokenId), }));batchTransfer({ address: nftBatchTransferAddress, abi:nftBatchTransferAbi.abi, functionName:"batchTransferFrom", args: [nftTransfers, unlockdAccount], });awaitfetchNFTsAndPrices(userAccount.address, selectedUnderlyingAsset, setInjectedWalletNFTs);awaitfetchNFTsAndPrices(unlockdAccount, selectedUnderlyingAsset, setUnlockdWalletNFTs); } catch (error) {console.error("Batch transfer failed:", error); } };
7. Set up borrowing functionality against selected NFTs.
Implement the core borrowing feature of the Unlockd protocol. This includes selecting NFTs from the Unlockd wallet, specifying borrow amounts, obtaining borrow signatures, and executing borrow transactions.
This example demonstrates how all the previously described functionalities are implemented within a single component for simplicity. However, this is just one approach. Feel free to adapt and restructure this implementation to best suit your own application's architecture and requirements.
// This function will create a borrow transaction.// The user should select the NFTs he wants to borrow against (limit: 100), and the amount he wants to borrow.// If the user has an unlockd wallet with allowed NFTs, he can borrow against them. // We 1st generate an array of nfts (collection and tokenId), after, we build the params we need to call the backend for a borrow signature, by providing the nfts and the underlying asset.
// The backend will return a signature, and we will use it to call the borrow function from the SDK, adding the amount, nfts and signature.
// we use 6 decimals since the borrow amount is in USDC.consthandleBorrow=async () => {if (selectedUnlockdNFTs.length===0||!token ||!unlockdAccount) {return; }try {constnftsArray=selectedUnlockdNFTs.map((nft) => ({ collection:nft.collection, tokenId:nft.tokenId, }));constparams= { nfts: nftsArray, underlyingAsset: selectedUnderlyingAsset, };constsignature=awaitapi.borrowSignature(token, params);awaitborrow(BigInt(parseUnits(borrowAmount,6)), nftsArray, signature, { network:Chains.Sepolia, });awaitfetchNFTsAndPrices(unlockdAccount, selectedUnderlyingAsset, setUnlockdWalletNFTs); } catch (error) {console.error("Borrow failed:", error); } };useEffect(() => {if (userAccount.isConnected &&userAccount.address &&isAddress(userAccount.address) ) {fetchNFTsAndPrices(userAccount.address, selectedUnderlyingAsset, setInjectedWalletNFTs); } }, [userAccount.isConnected,userAccount.address, selectedUnderlyingAsset]);useEffect(() => {if (token) {createOrRefreshUnlockdWallet(); } }, [token]);constisAuth=userAccount.isConnected &&!!token;
8. Render the user interface with all interactive elements
Create and render the user interface components that tie all the functionality together. This includes elements for wallet connection, NFT selection, approval buttons, transfer initiation, borrow amount input, and the borrow action button.