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
- Concepts
- Usage
- Installation
- Initialization
- Metadata
- First Time Connecting
- Second Time Connecting
- All Together
- Sending Requests
- Events
- Provider/Signer
- Types
- HashConnectTypes
- MessageTypes
- MessageTypes.BaseMessage
- MessageTypes.Acknowledge
- MessageTypes.Rejected
- MessageTypes.ApprovePairing
- MessageTypes.AdditionalAccountRequest
- MessageTypes.AdditionalAccountResponse
- MessageTypes.Transaction
- MessageTypes.TransactionMetadata
- MessageTypes.TransactionResponse
- MessageTypes.AuthenticationRequest
- MessageTypes.AuthenticationResponse
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)
}
This request sends an authentication response to the wallet which can be used to generate an authentication token for use with a backend system.
The expected use of this is as follows:
- generate a payload and signature on the server, this payload should contain a single-use code you can validate later
- send that payload and signature to the frontend
- send to the users wallet
- receive a new payload back along with the users signature of the new payload
- send this payload and user signature to your backend
- use this in your auth flow
This returns a AuthenticationResponse
await hashconnect.authenticate(topic, signingAcct, serverSigningAccount, serverSignature, payload);
Example Implementation:
async send() {
//!!!!!!!!!! DO NOT DO THIS ON THE CLIENT SIDE - YOU MUST SIGN THE PAYLOAD IN YOUR BACKEND
// after verified on the server, generate some sort of auth token to use with your backend
let payload = { url: "test.com", data: { token: "fufhr9e84hf9w8fehw9e8fhwo9e8fw938fw3o98fhjw3of" } };
let signing_data = this.SigningService.signData(payload);
//this line you should do client side, after generating the signed payload on the server
let res = await this.HashconnectService.hashconnect.authenticate(this.HashconnectService.saveData.topic, this.signingAcct, signing_data.serverSigningAccount, signing_data.signature, payload);
//send res to backend for validation
//THE FOLLOWING IS SERVER SIDE ONLY
let url = "https://testnet.mirrornode.hedera.com/api/v1/accounts/" + this.signingAcct;
fetch(url, { method: "GET" }).then(async accountInfoResponse => {
if (accountInfoResponse.ok) {
let data = await accountInfoResponse.json();
console.log("Got account info", data);
if(!res.signedPayload) return;
let server_key_verified = this.SigningService.verifyData(res.signedPayload.originalPayload, this.SigningService.publicKey, res.signedPayload.serverSignature as Uint8Array);
let user_key_verified = this.SigningService.verifyData(res.signedPayload, data.key.key, res.userSignature as Uint8Array);
if(server_key_verified && user_key_verified)
//authenticated
else
//not authenticated
} else {
alert("Error getting public key")
}
})
//!!!!!!!!!! DO NOT DO THIS ON THE CLIENT SIDE - YOU MUST PASS THE TRANSACTION BYTES TO THE SERVER AND VERIFY THERE
}
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
})
In accordance with HIP-338 and hethers.js we have added provider/signer support.
You need to initialize HashConnect normally, then once you have your hashconnect
variable you can use the .getProvider()
and .getSigner()
methods.
Just pass in these couple variables, and you'll get a provider back!
This allows you to interact using the API's detailed here.
provider = hashconnect.getProvider(network, topic, accountId);
Example Usage
let balance = await provider.getAccountBalance(accountId);
Pass the provider into this method to get a signer back, this allows you to interact with HashConnect using a simpler API.
signer = hashconnect.getSigner(provider);
Usage
let trans = await new TransferTransaction()
.addHbarTransfer(fromAccount, -1)
.addHbarTransfer(toAccount, 1)
.freezeWithSigner(this.signer);
let res = await trans.executeWithSigner(this.signer);
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
}
export interface AuthenticationRequest extends BaseMessage {
accountToSign: string;
serverSigningAccount: string;
serverSignature: Uint8Array | string;
payload: {
url: string,
data: any
}
}
export interface AuthenticationResponse extends BaseMessage {
success: boolean;
error?: string;
userSignature?: Uint8Array | string;
signedPayload?: {
serverSignature: Uint8Array | string,
originalPayload: {
url: string,
data: any
}
}
}