Hashconnect
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.
View Demo
Example React Integration
Concepts
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.
Usage
We recommend getting familiar with how async/await works before using Hashconnect. We also strongly suggest using Typescript.
Installation
npm i hashconnect --save
Initialization
Import the library like you would any npm package
ESM
import { HashConnect } from '@hashgraph/hashconnect';
CommonJS
import { HashConnect } from 'hashconnect/dist/cjs/main';
Create a variable to hold an instance of Hashconnect, pass true
to this to enable debug mode.
let hashconnect = new HashConnect();
Additional Steps to run Server-side
- When calling HashConnect.init(), a url must be defined in your app's metadata
const appMetadata = {
...,
url: "https://yourwebsite.com"
}
let initData = await this.hashconnect.init(appMetadata, "testnet", false);
Metadata
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.
Setup
All you need to do is create the HashConnect object, set up events, and then call the init function with your parameters.
let hashconnect = new HashConnect(true);
setUpHashConnectEvents();
let initData = await this.hashconnect.init(appMetadata, "testnet", false);
The init function will return your pairing code and any previously connected pairings as an array of SavedPairingData
(details).
Make sure you register your events before calling init - as some events will fire immediately after calling init.
Events
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.
FoundExtensionEvent
This event returns the metadata of the found extensions, will fire once for each extension.
hashconnect.foundExtensionEvent.once((walletMetadata) => {
})
FoundIframeEvent
If the app is embedded inside of HashPack it will fire this event. After this event is fired, it will automatically ask the user to pair and then fire a normal pairingEvent (below) with the same data a normal pairing event would fire.
PairingEvent
The pairing event is triggered when a user accepts a pairing. You can access the currently connected pairings from hashconnect.hcData.savedPairings
.
hashconnect.pairingEvent.once((pairingData) => {
})
Acknowledge Response
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) => {
})
Connection Status Change
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. This returns a HashConnectConnectionState
(details)
hashconnect.connectionStatusChangeEvent.once((connectionStatus) => {
})
Pairing
User the pairingString
to connect to HashPack - you can either display the string for the user to copy/paste into HashPack or use it to generate a QR code which they can scan. In the future, we will generate the QR for you but for now its your responsibility.
Pairing to extension
HashConnect has 1-click pairing with supported installed extensions. Currently the only supported wallet extension is HashPack.
When initializing any 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.
Second Time Connecting
When calling init HashConnect will automatically reconnect to any previously connected pairings. These pairings are returned in the init data (details).
Disconnecting
Call hashconnect.disconnect(topic)
to disconnect a pairing. You can then access the new list of current pairings from hashconnect.hcData.savedPairings
.
Sending Requests
Send Transaction
This request takes two parameters, topicID and Transaction.
await hashconnect.sendTransaction(initData.topic, transaction);
Example Implementation:
async sendTransaction(trans: Transaction, acctToSign: string) {
let transactionBytes: Uint8Array = await SigningService.signAndMakeBytes(trans);
const transaction: MessageTypes.Transaction = {
topic: initData.topic,
byteArray: transactionBytes,
metadata: {
accountToSign: acctToSign,
returnTransaction: false,
hideNft: false
}
}
let response = await hashconnect.sendTransaction(initData.topic, transaction)
}
Sign
This request allows you to get a signature on a generic piece of data. You can send a string or object.
await hashconnect.sign(initData.topic, signingAcct, dataToSign);
It will return a SigningResponse
Request Additional Accounts
This request takes two parameters, topicID and AdditionalAccountRequest. It is used to request additional accounts after the initial pairing.
await hashconnect.requestAdditionalAccounts(initData.topic, request);
Example Implementation:
async requestAdditionalAccounts(network: string) {
let request:MessageTypes.AdditionalAccountRequest = {
topic: initData.topic,
network: network
}
let response = await hashconnect.requestAdditionalAccounts(initData.topic, request);
}
Authenticate
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() {
let payload = { url: "test.com", data: { token: "fufhr9e84hf9w8fehw9e8fhwo9e8fw938fw3o98fhjw3of" } };
let signing_data = this.SigningService.signData(payload);
let res = await this.HashconnectService.hashconnect.authenticate(this.HashconnectService.initData.topic, this.signingAcct, signing_data.serverSigningAccount, signing_data.signature, payload);
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)
else
} else {
alert("Error getting public key")
}
})
}
Provider/Signer
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.
Get Provider
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);
Get Signer
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);
Types
HashConnectTypes
HashConnectTypes.AppMetadata
export interface AppMetadata {
name: string;
description: string;
url?: string;
icon: string;
encryptionKey?: string;
}
HashConnectTypes.WalletMetadata
export interface WalletMetadata {
name: string;
description: string;
url?: string;
icon: string;
encryptionKey?: string;
}
HashConnectTypes.InitilizationData
export interface InitilizationData {
topic: string;
pairingString: string;
encryptionKey: string;
savedPairings: SavedPairingData[]
}
HashConnectTypes.SavedPairingData
export interface SavedPairingData {
metadata: HashConnectTypes.AppMetadata | HashConnectTypes.WalletMetadata;
topic: string;
encryptionKey?: string;
network: string;
origin?: string;
accountIds: string[],
lastUsed: number;
}
HashConnectConnectionState
export enum HashConnectConnectionState {
Connecting="Connecting",
Connected="Connected",
Disconnected="Disconnected",
Paired="Paired"
}
MessageTypes
All messages types inherit topicID and ID from BaseMessage
MessageTypes.BaseMessage
export interface BaseMessage {
topic: string;
id: string;
}
MessageTypes.Acknowledge
export interface Acknowledge extends BaseMessage {
result: boolean;
msg_id: string;
}
MessageTypes.Rejected
export interface Rejected extends BaseMessage {
reason?: string;
msg_id: string;
}
MessageTypes.ApprovePairing
export interface ApprovePairing extends BaseMessage {
metadata: HashConnectTypes.WalletMetadata;
accountIds: string[];
network: string;
}
MessageTypes.AdditionalAccountRequest
export interface AdditionalAccountRequest extends BaseMessage {
network: string;
multiAccount: boolean;
}
MessageTypes.AdditionalAccountResponse
export interface AdditionalAccountResponse extends BaseMessage {
accountIds: string[];
network: string;
}
MessageTypes.Transaction
export interface Transaction extends BaseMessage {
byteArray: Uint8Array | string;
metadata: TransactionMetadata;
}
MessageTypes.TransactionMetadata
export class TransactionMetadata extends BaseMessage {
accountToSign: string;
returnTransaction: boolean;
hideNft?: boolean;
}
MessageTypes.TransactionResponse
export interface TransactionResponse extends BaseMessage {
success: boolean;
receipt?: Uint8Array | string;
signedTransaction?: Uint8Array | string;
error?: string;
}
MessageTypes.AuthenticationRequest
export interface AuthenticationRequest extends BaseMessage {
accountToSign: string;
serverSigningAccount: string;
serverSignature: Uint8Array | string;
payload: {
url: string,
data: any
}
}
MessageTypes.AuthenticationResponse
export interface AuthenticationResponse extends BaseMessage {
success: boolean;
error?: string;
userSignature?: Uint8Array | string;
signedPayload?: {
serverSignature: Uint8Array | string,
originalPayload: {
url: string,
data: any
}
}
}
MessageTypes.SigningRequest
export interface SigningRequest extends BaseMessage {
accountToSign: string;
payload: string | object
}
MessageTypes.SigningResponse
export interface SigningResponse extends BaseMessage {
success: boolean;
error?: string;
userSignature?: Uint8Array | string;
signedPayload?: string | object
}