hashport React Client
The hashport React Client contains a set of React contexts and hooks that allow you to add hashport's bridging functionality to your React application. This package is meant for those who want to integrate the functionality into their native user interface. If you want a styled plug-and-play solution, please take a look at the Widget here.
Documentation
Quick Start
Installation
Install the @hashport/react-client
package and its dependency @hashgraph/sdk
. Optionally install @rainbow-me/rainbowkit
and wagmi
if you plan to develop with RainbowKit
, or hashconnect
if you plan to develop with HashPack
.
npm install @hashport/react-client @hashgraph/sdk
# If using RainbowKit
npm install @hashport/react-client @hashgraph/sdk @rainbow-me/rainbowkit wagmi
# If using HashPack
npm install @hashport/react-client @hashgraph/sdk hashconnect
⚠ Note: @hashport/react-client
is a React library.
Import
Import the client based on your existing application environment.
If your React app already has an EVM signer (e.g. RainbowKit, MetaMask, ethers) AND a Hedera signer (e.g. Hashconnect, Hashgraph SDK), please use the HashportClientProvider
.
import { HashportClientProvider } from `@hashport/react-client/contexts`;
If your React app DOES NOT HAVE an EVM signer but has a Hedera signer, please use the HashportClientAndRainbowKitProvider
.
import { HashportClientAndRainbowKitProvider } from `@hashport/react-client/contexts`;
If your React app DOES NOT HAVE EITHER SIGNER, please integrate a Hedera signer first, such as Hashconnect.
Configure and Wrap Providers
Pass your EVM signer (if necessary) and Hedera signer as props to the provider that imported in the above step. Wrap your application with the configured provider.
const App = () => {
return (
<HashportClientProvider evmSigner={evmSigner} hederaSigner={hederaSigner}>
<YourApp />
</HashportClientProvider>
);
};
Add Functionality
You can start building out the rest of hashport's functionality into your app using the provided hooks documented below.
For more details, view the example we have created here.
Contexts
BridgeParamsProvider
BridgeParamsProvider
relies on the context feature of React to pass the necessary bridging parameters down to other components, so you need to make sure that BridgeParamsProvider
is a parent of the components you are encapsulating as part of the bridging functionality.
Props
children
The content of the component.
Type: React.ReactNode
HashportApiProvider
HashportApiProvider
relies on the context feature of React to pass the hashport API and core network objects down to other components, so you need to make sure that HashportApiProvider
is a parent of the components you are encapsulating to be able to consume these objects. HashportApiProvider
also initializes QueryClientProvider
from @tanstack/react-query to help simplify data fetching.
Props
children
The content of the component.
Type: React.ReactNode
mode
The API environment that the provider will set.
Type: 'mainnet' | 'testnet'
Default: 'mainnet'
HashportClientProvider
HashportClientProvider
relies on the context feature of React to pass core hashport functionality down to other components, so you need to make sure that HashportClientProvider
is a parent of the components you are encapsulating to be able to consume these objects. HashportClientProvider
also includes HashportApiProvider
and BridgeParamsProvider
to help simplify and improve your developer experience.
Props
children
The content of the component.
Type: React.ReactNode
evmSigner
An abstraction of an EVM Account, which can be used to sign messages and transactions and send signed transactions to the EVM Network.
Type: EvmSigner | undefined
hederaSigner
An abstraction of a Hedera Account, which can be used to sign messages and transactions and send signed transactions to the Hedera Network.
Type: HederaSigner | undefined
customMirrorNodeUrl
A URL to a custom Hedera mirror node service.
Type: string | undefined
Default: 'https://mainnet-public.mirrornode.hedera.com/api/v1'
customMirrorNodeCredentials
Credentials to customMirrorNodeUrl
(the custom Hedera mirror node service). The array indices should contain the following: ["your-x-api-key", "YOUR_API_KEY"]
.
⚠ If customMirrorNodeCredentials
is set without a customMirrorNodeUrl
defined, the HashportClientProvider
will not work.
Type: [string, string] | undefined
debug
If true
, enables the logger.
Type: boolean
Default: false
mode
The API environment that the provider will set.
Type: 'mainnet' | 'testnet'
Default: 'mainnet'
persistOptions
Persistent storage options for ongoing transaction state. This allows users to continue their bridging process after an error.
Type: { persistKey?: string | undefined; storage?: StateStorage | undefined; } | undefined
Default: { persistKey: 'hashportTransactionStore', storage: localStorage }
disconnectedAccountsFallback
A fallback React node to be displayed if either the EVM Signer or the Hedera Signer has become disconnected.
Type: { disconnectedAccountsFallback?: React.ReactNode }
Default: <p>Please connect signers for both EVM and Hedera networks</p>
ProcessingTransactionProvider
ProcessingTransactionProvider
relies on the context feature of React to pass the state of the current processing transaction to other components. This provider depends on an instance of the HashportClient
, so be sure that ProcessingTransactionProvider
is a child of the HashportClientProvider
.
Props
children
The content of the component.
HashportClientAndRainbowKitProvider
HashportClientAndRainbowKitProvider
relies on the context feature of React to pass core hashport functionality and RainbowKit (Ethereum interface) down to other components, so you need to make sure that HashportClientAndRainbowKitProvider
is a parent of the components you are encapsulating to be able to consume these objects. HashportClientProvider
also includes HashportApiProvider
and BridgeParamsProvider
to help simplify and improve your developer experience.
Props
children
The content of the component.
Type: React.ReactNode
hederaSigner
An abstraction of a Hedera Account, which can be used to sign messages and transactions and send signed transactions to the Hedera Network.
Type: HederaSigner | undefined
customMirrorNodeUrl
A URL to a custom Hedera mirror node service.
Type: string | undefined
Default: 'https://mainnet-public.mirrornode.hedera.com/api/v1'
customMirrorNodeCredentials
Credentials to customMirrorNodeUrl
(the custom Hedera mirror node service). The array indices should contain the following: ["your-x-api-key", "YOUR_API_KEY"]
.
⚠ If customMirrorNodeCredentials
is set without a customMirrorNodeUrl
defined, the HashportClientProvider
will not work.
Type: [string, string] | undefined
debug
If true
, enables the logger.
Type: boolean
Default: false
mode
The API environment that the provider will set.
Type: 'mainnet' | 'testnet'
Default: 'mainnet'
persistOptions
Persistent storage options for ongoing transaction state. This allows users to continue their bridging process after an error.
Type: { persistKey?: string | undefined; storage?: StateStorage | undefined; } | undefined
Default: { persistKey: 'hashportTransactionStore', storage: localStorage }
RainbowKitBoilerPlate
RainbowKitBoilerPlate
relies on the context feature of React to initialize and pass down Ethereum interfaces to other components, so you need to make sure that RainbowKitBoilerPlate
is a parent of the components you are encapsulating to be able to consume these interfaces. A configured WagmiConfig
and RainbowKitProvider
are provided to help simplify and improve your developer experience.
Props
children
The content of the component.
Type: React.ReactNode
chains
An array of chain objects that reference different EVM-compatible chains. By default, hashport-supported chains are included in the array.
⚠ Chain objects must adhere to the interface described here.
Type: RainbowKitChain[]
RainbowKit Props
Props of the RainbowKit component are also available.
Hooks
This package comes with a number of convenience hooks that help perform a hashport bridging transaction. The recommended usage is to set bridging parameters with useBridgeParamsDispatch
, queue up the transaction with useQueueHashportTransaction
, execute the transaction with useProcessingTransactionDispatch
, and monitor the status with useProcessingTransaction
. The hooks can be broken down into the following categories:
- Transaction Set-Up Hooks
- Transaction Execution Hooks
- Status Monitoring Hooks
- Account Connection Hooks
Transaction Set-Up Hooks
To set up a transaction, a user must define an amount, a recipient, a source asset, and a target asset. These parameters are then sent to the hashport API to obtain a list of steps that need to be executed in order to complete the transaction.
useBridgeParams
Returns a BridgeParams object.
Usage
const BridgeParamsDisplay = () => {
const { amount, sourceNetworkId, sourceAssetId, targetNetworkId, recipient } =
useBridgeParams();
return (
<div>
<p>Bridging Amount: {amount}</p>
<p>Source Chain id: {sourceNetworkId}</p>
<p>Source Asset for bridging: {sourceAssetId}</p>
<p>Target Chain id: {targetNetworkId}</p>
<p>Receiving account: {recipient}</p>
</div>
);
};
useBridgeParamsDispatch
Returns dispatch functions to set fields in the bridgeParams
object. Use with useTokenList
for easy token selection.
Usage
const SourceTokenSelection = () => {
const { sourceAssetId } = useBridgeParams();
const dispatch = useBridgeParamsDispatch();
const { data: tokens } = useTokenList();
const handleChange = e => {
const selectedToken = !tokens?.get(e.target.value);
if (selectedToken) return;
dispatch.setSourceAsset(selectedToken);
};
return tokens ? (
<select value={sourceAssetId}>
<option value="">--Select a token--</option>
{Array.from(tokens.fungible.entries()).map(([id, token]) => {
return (
<option key={id} value={id}>
{token.symbol}
</option>
);
})}
</select>
) : (
<p>Loading...</p>
);
};
⚠ Note: When setting the amount
for bridging, it's important to take the token's decimal into account. While EVM-based tokens can have up to 18 decimal places, Hedera tokens can only have up to 8. By default, the setAmount
dispatch function allows a maximum of 6 decimals. However, if a token has been selected, it will allow decimal precision as limited by the token. When the bridge params are submitted to the API, it is recommended to use the useQueueHashportTransaction
hook because it converts the decimal amount to wei or tinybar, which is what the API expects.
useTokenList
Returns a React Query result object where the data is an object that holds all supported assets on hashport, both fungible and nonfungible. Assets are stored as a Map of the token's unique ID to its respective AssetInfo.
⚠ Note: The token's unique id takes the following format: ${tokenIdOrAddress}-${chainId}
.
This hook also accepts an optional onSelect
function that is run when the handleSelect
function of an asset is called. It is recommended to use this hook with useBridgeParamsDispatch
to allow a user to select a token.
Usage
const TokenList = () => {
const {setSourceAsset} = useBridgeParamsDispatch();
const {data: tokens, isError, isLoading} = useTokenList({
onSelect(token) {
setSourceAsset(token);
}
})
if (isLoading) {
return <p>Loading...</p>;
} else if (isError) {
return <p>Error!</p>
} else {
return (
{Array.from(tokens.fungible.entries()).map(([uniqueId, {handleSelect, symbol, id, chainId}]) => (
<button key={uniqueId} onClick={handleSelect}>{symbol}</button>
))}
)
}
}
useSelectedTokens
This is simply a convenience hook wrapped around useTokenList
and useBridgeParams
. It removes some of the boiler plate needed to display the selected source and target tokens if they have been set in the bridgeParams
.
Usage
const SelectedTokens = () => {
const { sourceAsset, targetAsset } = useSelectedTokens();
return (
<div>
<p>{sourceAsset ? `Selected Asset: ${sourceAsset.symbol}` : 'Select a token!'}</p>
<p>
{targetAsset
? `Target Asset: ${targetAsset.symbol}`
: 'Where do you want to bridge to?'}
</p>
</div>
);
};
useTargetTokens
This is another convenience hook. If a source asset has been set, it will return an array of all the possible tokens you can bridge to. Returns undefined
if no source asset has been set.
⚠ Note: An additional assetId
property is added to each token to uniquely identify the token. This is helpful when providing a key
for React when mapping out the tokens.
Usage
const TargetTokens = () => {
const targetTokens = useTargetTokens();
const { setTargetAsset } = useBridgeParamsDispatch();
return (
<div>
{targetTokens ? (
<>
<p>Where would you like to bridge your asset?</p>
{targetTokens.map(token => {
return (
<button key={token.assetId} onClick={() => setTargetAsset(token)}>
{token.symbol}
</button>
);
})}
</>
) : (
<p>Choose a token to get started</p>
)}
</div>
);
};
useQueueHashportTransaction
This hook depends on the state provided by the BridgeParamsProvider
. If all the proper bridge parameters have been set by the user, this hook will return a function that:
- Converts the decimal amount to wei or tinybar.
- Submits the
bridgeParams
to the API and queues up the steps for execution.
⚠ Note: Be sure to add error handling to this function as it will throw an error if any of the parameters are set incorrectly.
If the function is called successfully, it will return a unique id
that you can pass to the execute
function on the hashportClient
or the executeTransaxtion
dispatch function from the useProcessingTransactionDispatch
hook.
Usage
const QueueTransaction = () => {
const queueTransaction = useQueueHashportTransaction();
const [queuedIds, setQueuedIds] = useState([]);
const handleQueueTransaction = async () => {
if (!queueTransaction) return;
const id = await queueTransaction();
setQueuedIds(prev => [...prev, id]);
};
return (
<div>
<h1>Queued Transactions</h1>
{queuedIds.map(id => {
<p key={id}>Transaction: {id}</p>;
})}
</div>
);
};
useMinAmount
Hashport imposes a minimum amount in order to initiate a bridging operation. If a set of bridge parameters does not meet this minimum, the API will not return the steps required to perform the transaction. However, to give the user a better experience, it's good to display the minimum porting amount.
⚠ Note: This hook adds a 10% buffer to the minimum amount. It's important to understand that the minimums are dynamic. If the prices change before a transaction reaches the validators, there is a chance that the transaction may fall below the minimum, which would result in a stuck transaction. As such, adding a buffer of an extra 10% helps mitigate that risk.
This hook depends on the state provided by the BridgeParamsProvider
. It reads the sourceAssetId
and sourceNetworkId
of the bridge parameters, fetches the minimum bridging amount, and returns it as a BigInt. If you want to display this number in its decimal form, you can get the decimals
of the token from the useTokenList
hook and use a function like viem's formatUnits
to convert it to a readable string.
Usage
import { formatUnits } from 'viem';
const MinimumAmountDisplay = () => {
const { data: minAmount, isLoading, isError } = useMinAmount();
const { sourceAssetId, sourceNetworkId } = useBridgeParams();
const { data: tokens } = useTokenList();
const selectedToken = tokens.fungible.get(`${sourceAssetId}-${sourceNetworkId}`);
if (isLoading || !selectedToken) {
return <p>Loading minimum amounts...</p>;
} else if (isError) {
return <p>Failed to get minimum amounts!</p>;
} else if (minAmount === undefined) {
return <p>Please select a token</p>;
} else {
return <p>Minimum bridging amount: {formatUnits(minAmount, selectedToken.decimals)}</p>;
}
};
usePreflightCheck
This hook depends on the HashportClientProvider
and the BridgeParamsProvider
. Use this hook to display the state of the user's bridging parameters and whether or not they will be able to execute the transaction. It checks the following:
- Whether or not the user has enough balance to complete the transaction
- Whether or not the user meets the minimum amounts
Returns {isValidParams: boolean; message?: string}
where the message
is only defined if isValidParams
is false
.
Usage
const PreflightCheckMessage = () => {
const preflightStatus = usePreflightCheck();
return preflightStatus.isValidParams ? (
<p>Ready to bridge!</p>
) : (
<p>Error: {preflightStatus.message}</p>
);
};
Transaction Execution Hooks
Once you have set up the proper bridge parameters and queued the transaction, all that's left to do is execute the transaction. The recommended way is to use the executeTransaction
callback provided by the useProcessingTransactionDispatch
hook. These functions are useful for displaying the current state of a submitted transaction.
useProcessingTransactionDispatch
This hook returns an executeTransaction
callback to be used with the useQueueHashportTransaction
hook. It also returns a confirmCompletion
callback which is helpful for cleaning up bridgeParams
state.
Usage
const ExecuteButton = () => {
const { executeTransaction } = useProcessingTransactionDispatch();
const queueTransaction = useQueueHashportTransaction();
const handleExecute = async () => {
try {
if (!queueTransaction) throw 'Set bridge parameters first';
const id = await queueTransaction();
const confirmation = await executeTransaction();
} catch (error) {
console.error(error);
}
};
return <button onClick={handleExecute}>Execute</button>;
};
useHashportClient
If you prefer a more manual approach, you can also use this hook. It returns an instance of the hashportClient
from the @hashport/sdk
. It depends on the HashportClientProvider](#hashportclientprovider). It is recommended to use this with the [
useQueueHashportTransaction` hook.
⚠ Note: Be sure to add error handling to this function in case there are network issues or if the user rejects a wallet interaction.
Usage
const ExecuteButton = () => {
const hashportClient = useHashportClient();
const queueTransaction = useQueueHashportTransaction();
const [id, setId] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const handleExecute = async () => {
try {
if (!queueTransaction) {
setErrorMessage('Set bridge parameters first');
}
setErrorMessage('');
let tempId = id;
if (!tempId) {
tempId = await queueTransaction();
setId(tempId);
}
const confirmation = await hashportClient.execute(tempId);
console.log('Completed transaction: ', confirmation);
} catch (error) {
console.error(error);
setErrorMessage(error.message);
}
};
return (
<div>
{errorMessage ? <p>{errorMessage}</p> : null}
<button onClick={handleExecute} disabled={!queueTransaction}>
Execute
</button>
</div>
);
};
Status Monitoring Hooks
There are a number of steps that must be completed in order to transfer assets across the hashport bridge. These steps may involve hedera transactions, EVM transactions, waiting for block confirmations, etc. To help keep the user updated on the progress of their transaction, you can use the following hooks.
useProcessingTransaction
Returns a ProcessingTransactionState
object. Statuses can be 'idle'
, 'processing'
, 'complete'
, or 'error'
. It also returns the id
of the current transaction as well as the currentTransaction
data related to that id
.
Use this with the getStepDescription
function to get a brief description of the current step the user is on.
⚠ Note: You can use the tokenAssociationStatus
property on the transaction state to pass a boolean as a second argument to getStepDescription
. This will help prompt the user to accept a token association request in their wallet if needed.
Usage
const ProcessingTransaction = () => {
const { status, id, confirmation, error, currentTransaction } = useProcessingTransaction();
if (status === 'idle') {
return <p>Choose tokens to start bridging!</p>;
} else if (status === 'processing' && currentTransaction.steps) {
const isAssociating = currentTransaction.state.tokenAssociationStatus === 'ASSOCIATING';
return <p>{getStepDescription(currentTransaction.steps[0])}</p>;
} else if (stats === 'complete') {
return <p>Complete: {confirmation.confirmationTransactionHashOrId}</p>;
} else {
return <p>Error: {error}</p>;
}
};
useBlockConfirmations
To ensure the safety of a user's transaction, the validators wait for a designated number of block confirmations before validating the transactions. This hook can be used to give users an update on the number of confirmations that must pass before their transaction can be completed. Returns a React Query result object where the data is the number of block confirmations for the given chainId.
Usage
const BlockConfirmations = () => {
const { evmSigner } = useHashportClient();
const chainId = evmSigner.getChainId();
const { data: blockConfirmations, isLoading, isError } = useBlockConfirmations(chainId);
if (isLoading) {
return <p>Loading...</p>;
} else if (isError) {
return <p>Error!</p>;
} else {
return <p>Block confirmations: {blockConfirmations.toString()}</p>;
}
};
Account Connection Hooks
Hooks in this section help with connecting wallets for EVM and Hedera accounts.
useHashConnect
This is a simple wrapper around the hashconnect
package. Because it creates a new instance of hashconnect
with each call, it is recommended that this hook only be used to instantiate the hashportClient
. If you need to use it throughout the application, place it in a context to maintain referential equality.
Usage
const HashportProvider = ({ children }: { children: React.ReactNode }) => {
const { hashConnect, pairingData, status } = useHashConnect();
const hederaSigner =
hashConnect && pairingData && createHashPackSigner(hashconnect, pairingData);
return (
<HashportClientProviderWithRainbowKit hederaSigner={hederaSigner}>
<p>Hashpack Connection Status: {status}</p>
{children}
</HashportClientProviderWithRainbowKit>
);
};
Examples
A full example of @hashport/react-client
in use is available in the examples
directory. We currently only have an example that uses Vite. If you have an example you would like to contribute, consider making a PR!
Development Environment
To set up your development environment, you will need the following:
- Hedera Testnet account (create a new account here). Testnet accounts are topped off with 10,000 testnet HBAR every 24 hours.
- EVM Testnet account (a list of supported testnet chains can be found here, with Sepolia being the most recommended).
- EVM Testnet faucet funds for gas fees. You can get Sepolia ETH from Alchemy's faucet.
- Sufficient balance for each test token (Visit the swagger documentation to see what tokens are supported. Then visit the respective blockchain explorer and interact with the contract to mint some tokens to your testnet account(s).)
⚠ Note: The Hedera Testnet resets every quarter, which erases all previous data and tokens. You'll need to update the Hedera Testnet tokens each time there is a reset. Learn more here.
After you have set up your testnet accounts, you can initialize the hashportClient
by connecting your wallets or using the hederaSdkSigner
and localEvmSigner
provided by the @hashport/sdk
. Be sure to set the mode
on the client to "testnet"
!
Troubleshooting
Polyfills
Libraries like Hashconnect and RainbowKit rely on a few node-specific packages. Refer to RainbowKit's documentation to learn about whether or not you need to include polyfills and how to do so. You can also refer to the Vite example in the [examples
] directory for a minimal example.