DocumentationLedger LiveAccountsIntegration walkthroughsBlockchain2 - Embedded App JS Bindings

2 - Embedded App JS Bindings

You will need to provide a JS implementation to interact with your coin embedded app on a Ledger. These bindings can either be implemented directly into live-common (as a folder in your coin family folder), or published in LedgerJS/packages as a package - i.e. hw-app-mycoin.

Minimal Implementation

The app implementation should provide at least 2 methods:

  • getAddress(path: string, display?: bool): derive an address providing the BIP32 path
  • signTransaction(path: string, message: string): sign a raw message for provided BIP32 path
⚠️

If the display argument in getAddress doesn’t work, get in touch with us - a necessary change may need to be made to the Ledger embedded app.

đź’ˇ

Arguments are provided as example, but try to follow as much as possible other implementations for easy integration. If stuck, find and read your coin’s embedded app documentation (doc/api.asc).

Any features provided by the embedded app should be provided through these JS bindings, such as getAppConfiguration or any blockchain-specific capabilities.

Example

You can find many implementations (hw-app-*) in LedgerJS

 
import type Transport from "@ledgerhq/hw-transport";
import BIPPath from "bip32-path";
import { UserRefusedOnDevice, UserRefusedAddress } from "@ledgerhq/errors";
 
const CHUNK_SIZE = 250;
 
const CLA = 0xe0;
const INS = {
  GET_VERSION: 0x00,
  GET_ADDR: 0x01,
  SIGN: 0x02,
};
const PAYLOAD_TYPE_INIT = 0x00;
const PAYLOAD_TYPE_ADD = 0x01;
const PAYLOAD_TYPE_LAST = 0x02;
 
const SW_OK = 0x9000;
const SW_CANCEL = 0x6986;
 
/**
 * MyCoin App API
 */
export default class MyCoin {
  transport: Transport;
 
  constructor(transport: Transport, scrambleKey: string = "MYC") {
    this.transport = transport;
    transport.decorateAppAPIMethods(
      this,
      ["getAddress", "signTransaction", "getAppConfiguration"],
      scrambleKey
    );
  }
 
  /**
   * Serialize a bip path to data buffer
   */
  serializePath(path: Array<number>): Buffer {
    const data = Buffer.alloc(1 + path.length * 4);
 
    data.writeInt8(path.length, 0);
    path.forEach((segment, index) => {
      data.writeUInt32BE(segment, 1 + index * 4);
    });
 
    return data;
  }
 
  /**
   * Iterates callback over chunks and return a single Promise
   */
  foreach<T, A>(arr: T[], callback: (T, number) => Promise<A>): Promise<A[]> {
    function iterate(index, array, result) {
      if (index >= array.length) {
        return result;
      } else
        return callback(array[index], index).then(function (res) {
          result.push(res);
          return iterate(index + 1, array, result);
        });
    }
    return Promise.resolve().then(() => iterate(0, arr, []));
  }
 
  /**
   * Get MyCoin address for a given BIP 32 path.
   *
   * @param path a path in BIP 32 format
   * @param display optionally enable or not the display
   * @return an object with a publicKey, address
   * @example
   * const result = await myCoin.getAddress("44'/8008'/0'/0/0");
   * const { publicKey, address, returnCode } = result;
   */
  async getAddress(
    path: string,
    display?: boolean
  ): Promise<{
    publicKey: string,
    address: string,
    returnCode: number,
  }> {
    const bipPath = BIPPath.fromString(path).toPathArray();
    const serializedPath = this.serializePath(bipPath);
 
    const p1 = display ? 0x01 : 0x00;
    const p2 = 0x00;
    const statusList = [SW_OK, SW_CANCEL];
 
    const response = await this.transport.send(
      CLA,
      INS.GET_ADDR,
      p1,
      p2,
      serializedPath,
      statusList
    );
 
    const errorCodeData = response.slice(-2);
    const returnCode = errorCodeData[0] * 0x100 + errorCodeData[1];
 
    if (returnCode === SW_CANCEL) {
      throw new UserRefusedAddress();
    }
 
    return {
      publicKey: response.slice(0, 32).toString("hex"),
      address: response.slice(32, response.length - 2).toString("ascii"),
      returnCode,
    };
  }
 
  /**
   * Sign a MyCoin transaction with a given BIP 32 path
   *
   * @param path a path in BIP 32 format
   * @param message a raw hex string representing a serialized transaction.
   * @return an object with signature and returnCode
   */
  async signTransaction(
    path: string,
    message: string
  ): Promise<{ signature: null | Buffer, returnCode: number }> {
    const bipPath = BIPPath.fromString(path).toPathArray();
    const serializedPath = this.serializePath(bipPath);
 
    const chunks = [];
    chunks.push(serializedPath);
    const buffer = Buffer.from(message);
 
    for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
      let end = i + CHUNK_SIZE;
      if (i > buffer.length) {
        end = buffer.length;
      }
      chunks.push(buffer.slice(i, end));
    }
 
    let response = {};
 
    return this.foreach(chunks, (data, j) =>
      this.transport
        .send(
          CLA,
          INS.SIGN,
          j === 0
            ? PAYLOAD_TYPE_INIT
            : j + 1 === chunks.length
            ? PAYLOAD_TYPE_LAST
            : PAYLOAD_TYPE_ADD,
          0,
          data,
          [SW_OK, SW_CANCEL]
        )
        .then((apduResponse) => (response = apduResponse))
    ).then(() => {
      const errorCodeData = response.slice(-2);
      const returnCode = errorCodeData[0] * 0x100 + errorCodeData[1];
 
      let signature = null;
      if (response.length > 2) {
        signature = response.slice(0, response.length - 2);
      }
 
      if (returnCode === SW_CANCEL) {
        throw new UserRefusedOnDevice();
      }
 
      return {
        signature,
        returnCode,
      };
    });
  }
 
  /**
   * get the version of the MyCoin app installed on the hardware device
   *
   * @return an object with a version
   * @example
   * const result = await myCoin.getAppConfiguration();
   *
   * {
   *   "version": "1.0.0"
   * }
   */
  async getAppConfiguration(): Promise<{
    version: string,
  }> {
    const response = await this.transport.send(
      CLA,
      INS.GET_VERSION,
      0x00,
      0x00
    );
    const result = {};
    result.version = "" + response[1] + "." + response[2] + "." + response[3];
    return result;
  }
}

Usage Example

 import MyCoinSdk from "my-coin-sdk"; // Your coin js sdk
 import Transport from "@ledgerhq/hw-transport-node-hid";
 // import Transport from "@ledgerhq/hw-transport-u2f"; // for browser
 import MyCoin from "@ledgerhq/hw-app-mycoin"; // App JS Bindings
 
 function establishConnection() {
     return Transport.create()
         .then(transport => new MyCoin(transport));
 }
 
 function fetchAddress(myCoin) {
     return myCoin.getAddress("44'/8008'/0'/0/0");
 }
 
 function signTransaction(myCoin, deviceData, nonce) {
     let tx = {
         type: "send",
         from: deviceData.address,
         to: "destination-address",
         amount: "1000000",
         nonce,
     };
     const serialized = MyCoinSdk.encode(tx);
 
     console.log('Sending transaction to device for approval...');
     return myCoin.signTransaction("44'/8008'/0'/0/0", serialized);
 }
 
function prepareAndSign(myCoin, nonce) {
     return fetchAddress(myCoin)
         .then(deviceData => signTransaction(myCoin, deviceData, nonce));
}
 
establishConnection()
  .then(myCoin => prepareAndSign(myCoin, 123))
  .then(({ signature }) => console.log(`Signature: ${signature}`))
  .catch(e => console.log(`An error occurred (${e.message})`));
Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Nano S, Ledger Vault, Ledger OS are registered trademarks of Ledger SAS