Creating a Hedera wallet app

Learn how to get started with hederas SDK, using Angular and Nest.JS

Getting started

I will walk you trought how i created a hedera hashgraph wallet for receiving, sending and analyzing your wallet. For this project I used Angular, Nest.js and Amazon web services for deployment. This blog will have two main sections, on for the backend and one for the frontend. We will get started with the backend first as its much shorter.


Resources


Backend part

The backend part of things is pretty simple, we will need just one end-point for creating an account, this end-point will accept a mnemonics object that we will generate on the frontend. To get started install the Nest.JS Cli and start a new project.

$ npm i -g @nestjs/cli
$ nest new hedera-backend

After that we will add (incoming surprise) the hedera sdk package

$ npm i @hashgraph/sdk

Inside our app controler we need a /create route, heres the code:

  @Post('create')
  async createAccount(@Body() mnemonics: Mnemonic): Promise<any> {
    try {
      var account = await this.appService.createAccount(mnemonics);
      return account
    } catch (error) {
      return {
        error: 'Create account failed'
      }
    }
  }

This route will call our function in app.service.ts where we will create a new hedera account and return it to the frontend.


Inside our AppService we create the createAccount function where we pass in the mnemonics object that we use to generate a private key. We then return the private key, account id and the public key to the frontend. Here we are also getting our Hedera account ID and the private key from the env. variables. These will later come from the server but for now we can just create a .env file in the root of our project where we store the two values.

export class AppService {

  client: Client;

  globalAccountId = process.env.HEDERA_ACCOUNT;
  globalPrivateKey = process.env.HEDERA_PRIVATE;

  constructor() {}
  
  async createAccount(mnemonic: Mnemonic): Promise<IHederaAccount> {
    
    this.client = Client.forTestnet().setOperator(this.globalAccountId, this.globalPrivateKey);

    //* Create new keys
    const mnemonicObj = await Mnemonic.fromWords(mnemonic._mnemonic.words)
    const privateKey = await mnemonicObj.toLegacyPrivateKey();
    const publicKey = privateKey.publicKey;

    const newAccount = await new AccountCreateTransaction()
    .setKey(publicKey)
    .setInitialBalance(new Hbar(50))
    .execute(this.client);
 
    //* Get the new account ID
    const getReceipt = await newAccount.getReceipt(this.client);
    const newAccountId = getReceipt.accountId;
    
    const account: IHederaAccount = {
      id: newAccountId.toString(),
      privateKey: privateKey.toStringRaw(),
      publicKey: publicKey.toStringRaw()
    } 

    return account
  }

And with this we are done with the backend part, simple right? We created one route /create to witch we will post the mnemonics object and in return get a object containing our Private key, Public key and Account ID.


Lets now look at the frontend to see how we can use this route.


Frontend part

For the frontend part we are using Angular so lets get started by installing the Angular CLI to easily create a boilerplate Angular application. And also add the hedera sdk.

$ npm install -g @angular/cli
$ ng new hedera-frontned
$ npm install --save @hashgraph/sdk

First we will need some sort of a landing page, from there the user can register or sign in depanding on if he already has a account. All our data will get stored into the localStorage of the browser. So if the user already has a account object inside the localStorage then we will take them to a login screen, and if not the register form will appear, for my app I used the Taiga UI framework, you are welcome to pick your own framework or just go custome. We will focus more on the logic of the application otherwise this blog would be way to long.


Hedera service

Now lets create the Hedera Service with:

$ ng g s hedera-service

Here is where all the logic for interaction with hedera will go, i will put this in 2 parts for better clarity.

  client!: Client; // Hedera client
  userInfo?: IUser; // Information about the user (from LocalStorage)
  userSettings?: IUserSettings; // Extra settings set by the user (from LocalStorage)

  //? Used only on sign up
  password!: string;
  walletName!: string;

  private isLoggedIn$ = new BehaviorSubject<boolean>(false); // Behavior subject to keep track if the user is logged in
  _isLoggedIn = this.isLoggedIn$.asObservable();

  // Init the hedera client with the users id and privateKey  
  initClient(password: string): boolean {
    const getWallet = this.decryptUserInfo(password)
    if (getWallet) {
      this.userSettings = this.getUserSettings();
      this.userInfo = getWallet;
      this.password = password;
      if (this.userInfo.wallet.net === 'testnet') this.client = Client.forTestnet().setOperator(this.userInfo.wallet.id , this.userInfo.wallet.key);
      else this.client = Client.forMainnet().setOperator(this.userInfo.wallet.id , this.userInfo.wallet.key);
      this.isLoggedIn$.next(true);
      return true
    } else {
      return false
    }
  }

  // Query a public node to get a history of all our transactions 
  getAllTransactions(): Observable<any> {
    return this.http.get(`https://${this.userInfo?.wallet.net}.mirrornode.hedera.com/api/v1/transactions?account.id=${this.userInfo?.wallet.id}`)
  }

  // Call our create account end-point with mnemonics object 
  CreateAccount(mnemonic: Mnemonic, testNet: boolean): Observable<IHWalletDTO> {
    return this.http.post<IHWalletDTO>(environment.backendUrl + EHederApi.POST_NEW_ACCOUNT, mnemonic)
  }

  // Sending the transaction with model 
  //  export interface IHSendTransaction {
  //    from: string;
  //    to: string;
  //    amount: number;
  //  }
  async SendTransaciton(transaciton: IHSendTransaction): Promise<any> {
    const sendHbar = await new TransferTransaction()
    .addHbarTransfer(transaciton.from, new Hbar(-transaciton.amount))
    .addHbarTransfer(transaciton.to, new Hbar(transaciton.amount))
    .execute(this.client);

     const transactionReceipt = await sendHbar.getReceipt(this.client);
     return transactionReceipt;
  }

  // Get balance of current account
  async getAccountBalance(): Promise<AccountBalance | undefined> {
    if (this.userInfo) {
      const accountBalance = await new AccountBalanceQuery()
      .setAccountId(this.userInfo.wallet.id)
      .execute(this.client);
  
      return accountBalance;
    } else {
      return undefined
    }
  }

  // Used to check if the account your sending to actually exists
  async validateAccountId(accountId: string): Promise<any> {
    const query = new AccountInfoQuery()
    .setAccountId(accountId);

    const accountInfo = await query.execute(this.client);

    return accountInfo;
  }

  // Verify account id (used if user wants to import his account)
  async verifyAccount(wallet: IHImportWallet): Promise<any> {
    const client: Client = Client.forTestnet().setOperator(wallet.id, wallet.privateKey);

    const account = await new AccountInfoQuery()
    .setAccountId(wallet.id)
    .execute(client)

    return account;
  }

  // Featch the current price of HBAR
  getHbarPrice(): Observable<any> {
    return this.http.get('https://api.coingecko.com/api/v3/simple/price?' , {
      params: {
        ids: "hedera-hashgraph",
        vs_currencies: this.userSettings?.currency ?? 'usd'
      }
    })
  }

Next up are helper function

  // Decrypt users wallet info to be used in InitClient() function
  decryptUserInfo(password: string): IUser | undefined {
    const local = localStorage.getItem(ELocalStorage.userWallet);
    if (local) {
      const decData = CryptoJS.enc.Base64.parse(local).toString(CryptoJS.enc.Utf8)
      const decrypted = CryptoJS.AES.decrypt(decData, password).toString(CryptoJS.enc.Utf8);
      if (decrypted) {
        const wallet = JSON.parse(decrypted)
        if (wallet) {
          console.log(wallet)
          return wallet;
        }
      }
    }
    return undefined;
  }

  // Generate the mnemonics phrase to be sent to our createAccount end-point
  async generateMnemonics(): Promise<Mnemonic> {
    return await Mnemonic.generate();
  }

  // Check if the user already has his wallet data in the localStorage
  isWallet(): boolean {
    return localStorage.getItem(ELocalStorage.userWallet) ? true : false
  }

  // Sign out by passing the false value to our BehaviorSubject
  signOut(): void {
    this.userInfo = undefined;
    this.isLoggedIn$.next(false);
  }

  // Used to save the returned wallet from the backend to LocalStorage and encrypt with users set password
  saveUserWallet(wallet: IUser): void {
    if (this.password) {
      const encrypt = CryptoJS.AES.encrypt(JSON.stringify(wallet), this.password).toString()
      const encData = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encrypt))
      localStorage.setItem(ELocalStorage.userWallet, encData)
    } else {
      console.warn('No password while saving wallet')
    }
  }

  // Remove the wallet from localStorage
  removeUserWallet(): void {
    localStorage.removeItem(ELocalStorage.userWallet)
  }

  // Save and get users settings
  saveUserSettings(userSettings: IUserSettings): void {
    localStorage.setItem(ELocalStorage.userSettings, JSON.stringify(userSettings))
    this.userSettings = userSettings;
  }

  getUserSettings(): IUserSettings | undefined {
    const settings = localStorage.getItem(ELocalStorage.userSettings);
    if (settings) {
      return JSON.parse(settings);
    } 
    return undefined;
  }