Skip to content

Hashconnect library, readme in progress

Notifications You must be signed in to change notification settings

sommermeyer/hashconnect

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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. 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.

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

import { HashConnect } from 'hashconnect';

Create a variable to hold an instance of Hashconnect

let hashconnect = new HashConnect();

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.

First Time Connecting

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.

Pairing

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.

Pairing to extension

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.

Second Time Connecting

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.

All Together

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!

Sending Requests

All requests return a ID string, this can be used to track the request through acknowlege and approval/rejection events (next section).

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(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);
}

Send Transaction

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)
}

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() {
    //!!!!!!!!!! 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

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) => {
    //do something with metadata
})

PairingEvent

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);
    })
})

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) => {
    //do something with acknowledge response data
})

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.

hashconnect.connectionStatusChange.once((connectionStatus) => {
    //do something with connection status
})

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;
    publicKey?: string;
}
HashConnectTypes.WalletMetadata
export interface WalletMetadata {
    name: string;
    description: string;
    url?: string;
    icon: string;
    publicKey?: string;
}
HashConnectTypes.InitilizationData
export interface InitilizationData {
    privKey: string;
}
HashConnectTypes.ConnectionState
export interface ConnectionState {
    topic: string;
    expires: number;
}

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; //id of the message being acknowledged
}
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; //set to true if you want the wallet to return a signed transaction instead of executing it
    nftPreviewUrl?: string;
}
MessageTypes.TransactionResponse
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
}
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
        }
    }
}

About

Hashconnect library, readme in progress

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 81.2%
  • HTML 10.3%
  • SCSS 6.6%
  • JavaScript 1.9%