Hashconnect is a library to connect Hedera apps to wallets, similar to web3 functionality found in the Ethereum ecosystem.
The provided demo demonstrates the pairing and signing functionality. It also contains a demo wallet (testnet only) which can be used to test functionality during the alpha phase.
- Hashconnect
The main functionality of Hashconnect is to send Hedera transactions to a wallet to be signed and executed by a user - we assume you are familiar with the Hedera API's and SDK's used to build these transactions.
Hashconnect uses message relay nodes to communicate between apps. These nodes use something called a topic ID to publish/subscribe to. It is your responsibility to maintain (using localstorage or a cookie or something) topic ID's and hashconnect encryption keys between user visits.
Pairing is the term used to denote a connection between two apps. Generally pairing is the action of exchanging a topic ID and a metadata object.
We recommend getting familiar with how async/await works before using Hashconnect. We also strongly suggest using Typescript.
npm i hashconnect --save
Import the library like you would any npm package
import { HashConnect } from 'hashconnect';Create a variable to hold an instance of Hashconnect
let hashconnect = new HashConnect();You need to define some metadata so wallets can display what app is requesting an action from the user.
let appMetadata: HashConnectTypes.AppMetadata = {
name: "dApp Example",
description: "An example hedera dApp",
icon: "https://absolute.url/to/icon.png"
}The url of your app is auto-populated by HashConnect, to prevent spoofing.
Call init on the Hashconnect variable, passing in the metadata you've defined. This function returns an object containing a PrivateKey, you should save this for reuse on subsequent user visits.
let initData = await hashconnect.init(appMetadata);
let privateKey = initData.privKey; You then need to connect to a node, if this is the first time a user is connecting to a node you don't need to pass anything in to the connect function and it will generate a new topicID. If it's a returning user pass in the topic ID that the user was previously connected to.
let state = await hashconnect.connect();
let topic = state.topic;This function returns a state object, containing a new topicID (if you passed nothing in). Make sure you store this topic for reuse on subsequent user visits.
Please note, it is possible to connect to more than one wallet.
If this is the first time a user is pairing, you will need to generate a new pairing string. If it is not the first time a user is using your app you can skip this step and use the instructions in Second Time Connecting, as both apps will already be subscribed to the topic ID. Pass in the state variable from the .connect() function.
You can also specify what network you would like to request accounts for, either "testnet" or "mainnet".
The final argument is a boolean if you wish to have multiple accounts able to be selected or not, generally you'll want this to be false unless you want to use multiple account ID's in your app.
let pairingString = hashconnect.generatePairingString(state, "testnet", false);A pairing string is a base64 encoded string containing the topic to subscribe to and the metadata about your app.
When the users accepts it will fire a PairingEvent.
HashConnect has 1-click pairing with supported installed extensions. Currently the only supported wallet extension is HashPack.
Please note - this only works in SSL secured environments (https urls)
hashconnect.findLocalWallets();Calling this function will send a ping out, and supported wallets will return their metadata in a foundExtensionEvent (details). You should take this metadata, and display buttons with the available extensions. More extensions will be supported in the future!
You should then call:
hashconnect.connectToLocalWallet(pairingString, extensionMetadata);And it will pop up a modal in the extension allowing the user to pair.
When a user is returning for the second time you should automatically pair and begin listening for events. The functions from before all take optional parameters in order to reconnect.
Connecting a second time is much simpler, following the steps in First Time Connecting and saving the appropriate data you simply call init() and connect() with the appropriate parameters.
await hashconnect.init(appMetadata, saveData.privateKey);
await hashconnect.connect(saveData.topic, saveData.pairedWalletData!);If you wanted to reconnect to multiple wallets, you could run .connect() in a loop, using a different save data structure of course. You only need to run init() once.
So, once we put all this together this is what a rough initialization function would look like:
You can view the example app implementation here.
let hashconnect: HashConnect;
let saveData = {
topic: "",
pairingString: "",
privateKey: "",
pairedWalletData: null,
pairedAccounts: []
}
let appMetadata: HashConnectTypes.AppMetadata = {
name: "dApp Example",
description: "An example hedera dApp",
icon: "https://www.hashpack.app/img/logo.svg"
}
async initHashconnect() {
//create the hashconnect instance
hashconnect = new HashConnect();
if(!loadLocalData()){
//first init and store the private for later
let initData = await hashconnect.init(appMetadata);
saveData.privateKey = initData.privKey;
//then connect, storing the new topic for later
const state = await hashconnect.connect();
saveData.topic = state.topic;
//generate a pairing string, which you can display and generate a QR code from
saveData.pairingString = hashconnect.generatePairingString(state, "testnet", true);
//find any supported local wallets
hashconnect.findLocalWallets();
}
else {
//use loaded data for initialization + connection
await hashconnect.init(appMetadata, saveData.privateKey);
await hashconnect.connect(saveData.topic, saveData.pairedWalletData);
}
}
loadLocalData(): boolean {
let foundData = localStorage.getItem("hashconnectData");
if(foundData){
saveData = JSON.parse(foundData);
return true;
}
else
return false;
}
You'll need to add more to this code to get it working in your exact setup, but that's the jist of it!
All requests return a ID string, this can be used to track the request through acknowlege and approval/rejection events (next section).
This request takes two parameters, topicID and AdditionalAccountRequest. It is used to request additional accounts after the initial pairing.
await hashconnect.requestAdditionalAccounts(saveData.topic, request);Example Implementation:
async requestAdditionalAccounts(network: string) {
let request:MessageTypes.AdditionalAccountRequest = {
topic: saveData.topic,
network: network
}
let response = await hashconnect.requestAdditionalAccounts(saveData.topic, request);
}This request takes two parameters, topicID and Transaction.
await hashconnect.sendTransaction(saveData.topic, transaction);Example Implementation:
async sendTransaction(trans: Transaction, acctToSign: string) {
let transactionBytes: Uint8Array = await SigningService.signAndMakeBytes(trans);
const transaction: MessageTypes.Transaction = {
topic: saveData.topic,
byteArray: transactionBytes,
metadata: {
accountToSign: acctToSign,
returnTransaction: false
}
}
let response = await hashconnect.sendTransaction(saveData.topic, transaction)
}Events are emitted by HashConnect to let you know when a request has been fufilled.
You can listen to them by calling .on() or .once() on them. All events return typed data.
This event returns the metadata of the found extensions, will fire once for each extension.
hashconnect.foundExtensionEvent.once((walletMetadata) => {
//do something with metadata
})The pairing event is triggered when a user accepts a pairing. It returns an ApprovePairing object containing accountId's and a WalletMetadata.
hashconnect.pairingEvent.once((pairingData) => {
//example
pairingData.accountIds.forEach(id => {
if(pairedAccounts.indexOf(id) == -1)
pairedAccounts.push(id);
})
})This event returns an Acknowledge object. This happens after the wallet has recieved the request, generally you should consider a wallet disconnected if a request doesn't fire an acknowledgement after a few seconds and update the UI accordingly.
The object contains the ID of the message.
hashconnect.acknowledgeMessageEvent.once((acknowledgeData) => {
//do something with acknowledge response data
})This event is fired if the connection status changes, this should only really happen if the server goes down. HashConnect will automatically try to reconnect, once reconnected this event will fire again.
hashconnect.connectionStatusChange.once((connectionStatus) => {
//do something with connection status
})export interface AppMetadata {
name: string;
description: string;
url?: string;
icon: string;
publicKey?: string;
}export interface WalletMetadata {
name: string;
description: string;
url?: string;
icon: string;
publicKey?: string;
}export interface InitilizationData {
privKey: string;
}export interface ConnectionState {
topic: string;
expires: number;
}All messages types inherit topicID and ID from BaseMessage
export interface BaseMessage {
topic: string;
id: string;
} export interface Acknowledge extends BaseMessage {
result: boolean;
msg_id: string; //id of the message being acknowledged
}export interface Rejected extends BaseMessage {
reason?: string;
msg_id: string;
}export interface ApprovePairing extends BaseMessage {
metadata: HashConnectTypes.WalletMetadata;
accountIds: string[];
network: string;
}export interface AdditionalAccountRequest extends BaseMessage {
network: string;
multiAccount: boolean;
}export interface AdditionalAccountResponse extends BaseMessage {
accountIds: string[];
network: string;
}export interface Transaction extends BaseMessage {
byteArray: Uint8Array | string;
metadata: TransactionMetadata;
}export class TransactionMetadata extends BaseMessage {
accountToSign: string;
returnTransaction: boolean; //set to true if you want the wallet to return a signed transaction instead of executing it
nftPreviewUrl?: string;
}export interface TransactionResponse extends BaseMessage {
success: boolean;
receipt?: Uint8Array | string; //returns receipt on success
signedTransaction?: Uint8Array | string; //will return signed transaction rather than executing if returnTransaction in request is true
error?: string; //error code on response
}