Using the Hashport SDK & React Client

hashport
Coinmonks

--

In September, we introduced the Hashport SDK to the Hedera community. By making the SDK available to the public, it allowed developers of Hedera-native dApps to integrate Hashport directly onto their platforms.

The first piece in our Hashport SDK Series focused on the benefits of using the Hashport SDK. As a follow-up, in this blog, we will provide you with a step-by-step tutorial on installing and using the react client to build a minimal replica of the Hashport bridge using the @hashport/react-client package. By the end of this tutorial, you will have a simple React app that lets you bridge assets from the Hedera network to other supported EVM-compatible networks and back.

@hashport/react-client Tutorial

To get started, you must:

Please note that the @hashport/react-client package works only with React. If you are looking to use Hashport on the backend or a different framework, consider looking at the @hashport/sdk package.

Initialise a React App

To get started, open a terminal and the following command:

npm create vite@latest

This will run a CLI tool that walks you through setting up a project with Vite. We’ll name this project hashport-react-tutorial, choose React as our framework and the TypeScript + SWC option. It’ll prompt you to run the following commands:

cd hashport-react-tutorial
npm install
npm run dev

If you run those you’ll see a basic project open at localhost:3000 in your browser. Go ahead and Ctrl + C in the terminal to stop the application and open your favourite IDE. I’ll be using VS Code.

Installation

Now that we have our react app set up, we’ll need to install @hashport/react-client and its peer dependencies.

npm install @hashport/react-client @rainbow-me/rainbowkit wagmi @hashgraph/sdk hashconnect

The Hashport bridge allows us to move funds between supported EVM-compatible chains and the Hedera network. We need a way to connect to both of those, so our solution is to use RainbowKit (@rainbow-me/rainbowkit and wagmi) for EVM wallets and HashPack (@hashgraph/sdk and hashconnect) for Hedera wallets.

Note: Since v1 of RainbowKit came out, wagmi migrated from its dependency on ethers.js to viem, so why aren’t we installing that, too? The @hashport/react-client is built on the framework agnostic @hashport/sdk which has viem as a dependency. That means installing @hashport/react-client will also install viem.

Before we start writing anything, we have one more thing we need to take care of. Both RainbowKit and Hashconnect have dependencies on a few node modules that are supported in the browser. To see why this is a problem, go to App.tsx, strip out the contents of the App component and add the following:

import ‘./App.css’;
import { createHashPackSigner, useHashConnect, HashportClientProviderWithRainbowKit } from "@hashport/react-client";

const App = () => {
const { hashConnect, pairingData } = useHashConnect({ mode: 'testnet' });
const hederaSigner = pairingData && createHashPackSigner(hashConnect, pairingData);

return (
<HashportClientProviderWithRainbowKit mode="testnet"
hederaSigner={hederaSigner}>
Hello World!
</HashportClientProviderWithRainbowKit>
)
}

Now try to run npm run dev in the terminal. When you go to your browser, you’ll see… nothing! Let’s open the dev tools to see why. Right-click and choose “Inspect”. You’ll notice an error message saying that global is not defined. That’s because global is the NodeJS equivalent of the window object in the browser, but the browser doesn’t know what to do with this. Fortunately, RainbowKit has provided a simple solution for this. Create a new file called polyfills.ts and add the following:

import { Buffer } from ‘buffer’;

window.global = window.global ?? window;
window.Buffer = window.Buffer ?? Buffer;
window.process = window.process ?? { env: {} }; // Minimal process polyfill

export {};

Then go back to App.tsx and add this line at the top:

import “./polyfills”

Now if we try to run npm run dev again, you’ll see that everything loads up just fine! There’s a message saying Please connect signers for both EVM and Hedera networks, but we’ll take care of that in a minute. We’re finally ready to start developing!

Step 1: The Hashport contexts, RainbowKit, and HashPack

Now that things are working, let’s take a minute to understand what we have in App.tsx so far. The @hashport/react-client package uses React Context to manage state without having to pass props around. In this case, the HashportClientProviderWithRainbowKit component is a provider that gives us access to a number of utility hooks for managing state related to a transaction. It manages EVM Wallet connections under the hood with RainbowKit, but we still need to pass in a signer for Hedera. That’s where HashPack, the popular HBAR wallet, comes in. The @hashport/react-client library currently only supports Hedera connections with HashPack, but in the future, we plan to add more. (If you are up for the challenge, try submitting a PR here!) You can read up on how to connect HashPack with the hashconnect package, but to make things simpler, @hashport/react-client comes with a useHashConnect hook that manages connecting and disconnecting HashPack for you. One final thing to make note of is the mode prop that we pass to both the useHashConnect hook and the HashportClientProviderWithRainbowKit component. We pass testnet in so we can try making transactions without using real funds.

Note: hashconnect saves wallet connection data in localStorage but it does not reset itself if the app’s mode changes. If you are experiencing connection troubles when switching from testnet to mainnet and vice versa, try deleting the hashconnectData key in localStorage before reconnecting.

If we spin up the application, we get an error message asking us to connect our EVM and Hedera wallets. We can use the RainbowKit button to connect the EVM wallet, but what about the Hedera one? Update the code in App.tsx to match the following:

import ‘./App.css’;
import { createHashPackSigner, useHashConnect, HashportClientProviderWithRainbowKit } from "@hashport/react-client";
const App = () => {
const { hashConnect, pairingData } = useHashConnect({ mode: 'testnet' });
const hederaSigner = pairingData && createHashPackSigner(hashConnect, pairingData);
const accountId = pairingData?.accountIds[0];

return (
<HashportClientProviderWithRainbowKit
mode="testnet"
hederaSigner={hederaSigner}
renderConnectButton={(children, RainbowKitButton) => (
<main>
<h1>hashport</h1>
<div className="button-group">
<RainbowKitButton />
<button onClick={() => hashConnect.connectToLocalWallet()}>
{accountId ?? 'Connect HashPack'}
</button>
</div>
{children}
</main>
)}
>
<div className="container">
Hello World!
</div>
</HashportClientProviderWithRainbowKit>
)
}

Here we’ve used the renderConnectButton prop to define how we want our wallet connection buttons to be displayed. The prop takes a function with two arguments: the children (i.e., everything inside the provider) and the RainbowKitButton. Running npm run dev should now look something like this:

It looks weird with the buttons stacked like that, so replace the styles in App.css with this:

#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.container {
display: flex;
flex-direction: column;
gap: 0.5em
}

.button-group {
display: flex;
justify-content: center;
gap: 0.5em;
}

.status-text {
color: rgb(107, 129, 255);
font-style: italic;
}

h1,
h2,
h3,
h4,
h5,
h6,
p {
margin-block: 1rem
}

If you click the “Connect HashPack” button, your HashPack extension should pop open prompting you to connect. After connecting and updating the styles, you should have something like this:

There’s one more context we should add: ProcessingTransactionProvider. This context lets us use hooks related to the current state of a transaction. Depending on the kind of transaction, there may be a number of different steps, so it’s a good idea to update the user at each step. Add the following to App.tsx:

const App = () => {
// …

return (
<HashportClientProviderWithRainbowKit
// …
>
<ProcessingTransactionProvider>
Hello World!
</ProcessingTransactionProvider>
</HashportClientProviderWithRainbowKit>
)
}

With that, our application has everything needed to manage the state of a user’s transaction.

Step 2: Add token selection options and inputs

Now that we understand all the pieces that make it possible to develop with Hashport, let’s start adding some interactive elements. Create a new file SelectSource.tsx and add the following code:

import { AssetId, useBridgeParamsDispatch, useTokenList } from '@hashport/react-client';
import { ChangeEventHandler } from 'react';

export const SelectSource = () => {
const { setSourceAsset } = useBridgeParamsDispatch();
const { data: tokens } = useTokenList();

const handleChooseSource: ChangeEventHandler<HTMLSelectElement> = e => {
const sourceAsset = tokens?.fungible.get(e.target.value as AssetId);
sourceAsset && setSourceAsset(sourceAsset);
};

return tokens ? (
<select onChange={handleChooseSource}>
<option value=""> - Choose source asset - </option>
{Array.from(tokens.fungible.entries()).map(([id, token]) => (
<option key={id} value={id}>
{token.symbol}
</option>
))}
</select>
) : (
<p>loading tokens…</p>
);
};

The useTokenList hook fetches a list of all the tokens that are supported. It uses React Query under the hood, so you get all the benefits of their declarative API. Here, we just add a quick null check to make sure the data is there, and then we return the rest of the JSX. We’ve also added the useBridgeParamsDispatch. This hook returns callbacks for setting up a transaction. Here, we’ve defined a function handleChooseSource that takes care of selecting the sourceAsset (i.e., the token you want to bridge) by calling setSourceAsset. Pass that into the onChange property of select. Go back to App.tsx, import SelectSource.tsx and replace the “Hello World!” text with the component. If you run the app, you should see a small drop-down that has all the tokens Hashport supports.

Next, create a SelectTarget.tsx file and add this code:

import {
AssetId,
useBridgeParamsDispatch,
useSelectedTokens,
useTargetTokens,
useTokenList,
} from '@hashport/react-client';
import { ChangeEventHandler } from 'react';

export const SelectTarget = () => {
const { setTargetAsset } = useBridgeParamsDispatch();
const { data: tokens } = useTokenList();
const { targetAsset } = useSelectedTokens();
const targetTokens = useTargetTokens();

const handleChooseTarget: ChangeEventHandler<HTMLSelectElement> = e => {
const targetAsset = tokens?.fungible.get(e.target.value as AssetId);
targetAsset && setTargetAsset(targetAsset);
};

return tokens ? (
<select onChange={handleChooseTarget}>
<option value={''}> - To - </option>
{targetAsset?.bridgeableAssets.map(({ assetId }) => {
const asset = tokens?.fungible.get(assetId);
if (!asset) return;
return (
<option key={assetId} value={assetId}>
{asset.symbol}
</option>
);
})}
{targetTokens?.map(({ assetId, symbol }) => (
<option key={assetId} value={assetId}>
{symbol}
</option>
))}
</select>
) : (
<p>loading tokens…</p>
);
};

This code is similar to the SelectSource.tsx but there are two new hooks: useSelectedTokens and targetTokens. Each token on Hashport is only supported for a set number of target networks. The useTargetTokens hook makes it easy for us to show which tokens a user can bridge to. Note that it will only return a value if the user has already selected a source token. Then there’s the useSelectedTokens hook. This returns an object with two properties: sourceAsset and targetAsset. These represent the tokens a user has already chosen. Import this component in App.tsx and add it below the SelectSource component. Now, if you select a source token, you should be able to see all the possible tokens you can bridge in the “--To--” dropdown.

Time to add the amount input. Create an AmountInput.tsx file with this code:

import { useBridgeParamsDispatch, useSelectedTokens } from '@hashport/react-client';
import { ChangeEventHandler } from 'react';

export const AmountInput = () => {
const { sourceAsset, targetAsset } = useSelectedTokens();
const { setAmount } = useBridgeParamsDispatch();
const handleAmount: ChangeEventHandler<HTMLInputElement> = e => {
setAmount({
amount: e.target.value,
sourceAssetDecimals: sourceAsset?.decimals,
targetAssetDecimals: targetAsset?.decimals,
});
};
return <input placeholder="amount" onChange={handleAmount} />;
};

One thing to note about this file is the use of setAmount from useBridgeParamsDispatch. Tokens on the Hedera network have precision up to 8 decimals whereas EVM networks can have up to 18! This would obviously cause some issues if trying to bridge values that are too precise for Hedera to handle. So, we pass in the decimal places for each token so that the amount can be updated property. Import this component into App.tsx and place it right above the SelectSource component.

Next, we will add another input so we can define the receiving account. Add a new file RecipientInput.tsx with this:

import { useBridgeParamsDispatch } from '@hashport/react-client';
import { ChangeEventHandler } from 'react';

export const RecipientInput = () => {
const { setRecipient } = useBridgeParamsDispatch();

const handleRecipient: ChangeEventHandler<HTMLInputElement> = e => {
setRecipient(e.target.value);
};

return <input placeholder="recipient" onChange={handleRecipient} />;
};

It’s important to think about where the tokens will be going. If we are going from Hedera to EVM, we should provide our EVM address as a hex string. If going in the other direction, we should input our Hedera address, which will look something like this: 0.0.555555. Import the component into App.tsx and place it between AmountInput and SelectSource.

Step 3: Add some UX touches

We have everything we need to set up a transaction. But what can we do to display the status of a transaction? Create another file TransactionStatus.tsx and add this:

import { useProcessingTransaction } from '@hashport/react-client';

export const TransactionStatus = () => {
const processingTx = useProcessingTransaction();

return (
<p>
Transaction Status: <span className="status-text">{processingTx.status}</span>
</p>
);
};

We have a new hook! This one is called useProcessingTransaction and it gives us access to a bunch of information about the current transaction (Remember the ProcessingTransactionProvider context we added earlier? That’s where this data is coming from). For now, we will just grab the status off the return value and display it to the user. Let’s import this into App.tsx and stick it at the top, above AmountInput.

Note: The status can be one of four values: “idle”, “processing”, “error”, or “complete”. If it’s “error”, the hook will also return the error message that occurred during the transaction.

Step 4: Add the Execute button

The last component we will add is a button to execute the transaction, so create ExecuteButton.tsx:

import {
useBridgeParamsDispatch,
useProcessingTransaction,
useProcessingTransactionDispatch,
useQueueHashportTransaction,
} from '@hashport/react-client';

export const ExecuteButton = () => {
const { resetBridgeParams } = useBridgeParamsDispatch();
const queueTransaction = useQueueHashportTransaction();
const { executeTransaction, confirmCompletion } = useProcessingTransactionDispatch();
const processingTx = useProcessingTransaction();

const handleExecute = async () => {
if (!queueTransaction) return;
if (processingTx.status === 'complete') {
confirmCompletion();
resetBridgeParams();
return;
}
try {
if (processingTx.id) {
await executeTransaction(processingTx.id);
} else {
const id = await queueTransaction();
await executeTransaction(id);
}
} catch (error) {
console.log(error);
}
};

return (
<button
disabled={!queueTransaction || processingTx.status === 'processing'}
onClick={handleExecute}
>
{processingTx.status === 'processing'
? 'In progress…'
: processingTx.status === 'complete'
? 'Confirm'
: 'Execute'}
</button>
);
};

This one has a lot more going on, so let’s break it down starting with the handleExecute function. First, we make sure that queueTransaction is defined. This comes from the useQueueHashportTransaction hook, which validates all the bridge parameters and fetches a list of steps required to execute a transaction. If any of the parameters are unset, it will return undefined. Next, we check the transaction status. This uses the same hook we saw earlier: useProcessingTransaction. If the status is ‘complete’, we should clear out any leftover state. So, we call confirmCompletion from useProcessingTransactionDispatch and resetBridgeParams from useBridgeParamsDispatch. After that, we check to see if processingTx.id is defined. This is helpful for recovering transactions that encountered an error. If there’s an id, we have a transaction on the queue that we need to complete. We do that by passing this id to executeTransaction, which we got from useProcessingTransactionDispatch. Where does this id come from, you ask? If we look at the next line, we can see that it comes from queueTransaction. When a transaction is queued, this id is generated to internally keep track of transactions. While it’s only possible to execute one transaction at a time, we can queue up multiple transactions. For now, though, we will just stick to queuing and executing one at a time. Finally, we take this handleExecute callback and pass it to the onClick of the button in the return statement. We then add a few conditions to disable the button so we can’t click it if the params aren’t set or if we are already working on a transaction. Import this last component into App.tsx at the bottom of the containing div.

Step 5: Submit a transaction!

We are ready to try our first transaction! But before we do that, we need to get some funds! If you don’t have one already, head over to the Hedera portal to create a testnet account. These accounts are topped off with 10,000 testnet HBAR every 24 hours, which is plenty for what we’ll be doing. Next, if you don’t already have an EVM account, create one in MetaMask. We’ll need to make sure our EVM account has enough ETH to pay for gas. We’ll be testing things out on Sepolia Testnet, so head over to the Sepolia Faucet to have 0.5 Sepolia ETH deposited into your account.

Note: ALWAYS pay attention to what networks you are on and what transactions you are signing for. In our case, we are using testnet, but if the mode prop is omitted in the useHashConnect hook and HashportClientProviderWithRainbowKit, it defaults to mainnet.

With your testnet accounts, connect to the app we’ve created and select HBAR as the source and HBAR[sep] as the target. Enter an amount that’s above the minimum (around 1000 should suffice), paste in your EVM address, and hit that “Submit” button!

You should get a request pop up in your HashPack wallet to execute the deposit transaction. Accept it and follow the rest of the prompts. Congratulations! You just built a bridging app! The only thing left is to get this ready for production.

Step 6: Create a build

We’re almost done. The last thing to do is create a build. Run this command:

npm run build && npm run preview

This will create a build that’s ready for production and output the files into a new directory called dist. The preview command serves the files from the build so you can confirm that everything is working as intended. If you want to serve this as a static frontend application, just serve the contents of the newly created dist directory.

Conclusion

To summarise, we used the @hashport/react-client library to set up a simple application that lets you bridge assets from the Hedera network to and from supported EVM networks. This is just a minimal example; there are lots of things you can do to improve the user experience. If you run into any issues along the way, create an issue on GitHub. Thanks for reading, and have fun building!

Feel free to reach out via our contact form or slide into our Twitter (X) DMs if you’re interested in learning more.

Hashport SDK Series:
1. Introducing the Hashport SDK
2. Using the Hashport SDK & React Client
3. Installing and Using the Hashport Widget

About Hashport
Hashport is the enterprise-grade public utility that facilitates the movement of digital assets between distributed networks, extending their functionality in a quick, secure, and cost-effective way. In order to remain platform-neutral, Hashport functions without the use of a proprietary token. The network is built on a robust and performant architecture, secured and operated by a group of industry-leading validator partners from around the world. Hashport has passed rigorous security audits and follows industry best practices; regularly performing comprehensive network tests to ensure the integrity of the network.

Website | Twitter | Reddit | Telegram | LinkedIn | YouTube | GitHub

Disclaimer: The information provided on Hashport’s website does not constitute investment advice, financial advice, trading advice, or any other sort of advice. You should not treat any site content as advice.

--

--

hashport
Coinmonks

✨Hashport is a public utility enabling fast & secure cross-network token transfers. $HBAR $ETH $MATIC $AVAX $BNB $OP #Arbitrum #Fantom #Cronos #Moonbeam #Aurora