Documentation
2 - 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 (opens in a new tab) 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 (opens in a new tab)

 
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, Bolos are registered trademarks of Ledger SAS