LedgerHQ / ledger-wallet-api

High level API to the Ledger Wallet Chrome app
82 stars 35 forks source link
infraprj-4968

High level API to the Ledger Wallet Chrome app

This is the high level JS API to the Ledger Wallet Chrome application. With this API, you can communicate directly with the Chrome app and use embedded features such as requesting an address to receive bitcoins, or send a Bitcoin transaction.

Each call to the API will trigger an UI response to the Chrome app, where the user will be able to validate the request.

Demonstration

A very simple demonstration of the API calls is available here
https://www.ledgerwallet.com/api/demo.html

Usage

First, you need to insert the ledger.js file in your page:

<head>
  <script src="https://github.com/LedgerHQ/ledger-wallet-api/raw/master/ledger.js"></script>
</head>

Initialization of the Ledger object is as follow:

function callback(event) {
  console.log(event.response);
};
Ledger.init({ callback: callback });

This will create an invisible iframe on the ledgerwallet.com domain, acting as a proxy between your web page and the Ledger Chrome app. Each time you request an API call, a message will be sent to the Chrome app and the response will be sent to the callback function.

All calls are asynchronous, and you are not garanteed to get a callback (for instance, if the user kills the Chrome app). If you are using a button to trigger a call, it is recommended to disable the action button for a few seconds using for instance setTimeout(enableButtonFunction, 4000).

API calls

  1. isAppAvailable()
  2. launchApp()
  3. hasSession()
  4. getAccounts()
  5. getOperations(account_id)
  6. getNewAddresses(account_id, count)
  7. sendPayment(address, amount, data)
  8. getXPubKey(path)
  9. signP2SH(inputs, scripts, outputs_number, outputs_script, paths)
  10. bitid(uri, silent)

===

Ledger.isAppAvailable()

Check if the Ledger Chrome app is installed.

function callback(event) {
  if (!event) {
    console.log("Chrome app not available");
  } else {
    response = event.response;
    if (response.command == "ping") {
      console.log("Chrome app is available.");
    }
  }
};
Ledger.init({ callback: callback });
Ledger.isAppAvailable();

If the Ledger Chrome app is not available, the returned event will be undefined. If it is available it will return the following event.response:

{  
   "command":"ping",
   "result":true
}

All following API calls will return undefined if the Ledger Chrome app is not installed. In the next examples, it is taken for granted that the Chrome app is installed, and therefore we are not checking the existence of event.response.

===

Ledger.launchApp()

Launch the Ledger Chrome application.

function callback(event) {
  response = event.response;
  if (response.command == "launch") {
    console.log("Chrome app has been launched.");
  }
};
Ledger.init({ callback: callback });
Ledger.launchApp();

It will always return the following response after having launched the app:

{  
   "command":"launch",
   "result":true
}

You usually don't have to use this call, as it will be used by other calls automatically.

===

Ledger.hasSession()

Check if the wallet is initialized and ready (Nano has been inserted, correct PIN entered, and wallet synchronized).

function callback(event) {
  response = event.response;
  if (response.command == "has_session") {
    if (response.result) {
      console.log("Wallet is ready");
    } else {
      console.log("Wallet is NOT ready");
    }
  }
};
Ledger.init({ callback: callback });
Ledger.hasSession();

If session is ready:

{  
   "command":"has_session",
   "result":true
}

If session is not ready:

{  
   "command":"has_session",
   "result":false
}

You normally don't need to use this call. All following API calls require the wallet to be ready. They will therefore automatically check that app has been launched and wait for the session to be ready by pooling the hasSession() call.

===

Ledger.getAccounts()

Request export of your list of accounts (one account is a HD wallet account, including packs of public addresses).

function callback(event) {
  response = event.response;
  if (response.command == "get_accounts") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
Ledger.getAccounts();

If user cancels the request:

{
   "command":"get_accounts",
   "success":false,
   "message":"Request cancelled by the user"
}

If user grants the request:

{
   "command":"get_accounts",
   "success":true,
   "accounts":[
      {
         "index":0,
         "name":"My account",
         "hidden":false,
         "color":"#FF5254",
         "wallet_id":1,
         "total_balance":0,
         "root_path":"44'/0'/0'"
      }
   ]
}

Use the value of index for account_id in getOperations.

All balances are in satoshis.

===

Ledger.getOperations(account_id)

Request export of all operations (incoming and outgoing transactions) for account account_id.

function callback(event) {
  response = event.response;
  if (response.command == "get_operations") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
Ledger.getOperations(1);

If user cancels the request:

{
   "command":"get_operations",
   "success":false,
   "message":"Request cancelled by the user"
}

If user grants the request:

{  
   "command":"get_operations",
   "success":true,
   "operations":[  
      {  
         "hash":"f5263795501685d5f10ec08a37f3caceb4e215d8632f5cfd325faaa271675cde",
         "fees":50000,
         "time":1429643779892,
         "type":"reception",
         "value":150000,
         "confirmations":333,
         "senders":[  
            "3882TpSheaxuHESqNyMW8L3BA33qirEPYt",
            "3PQhX99dAH2rowwMYfPP1KWKRbdCn55cjJ"
         ],
         "recipients":[  
            "15Tc1vFcBQJD5vCs5sZM3s5cAZewLs8sM5"
         ],
         "id":81,
         "account_id":1
      },
      { }
   ]
}

===

Ledger.getNewAddresses(account_id, count)

Request export of count new addresses for account account_id.

function callback(event) {
  response = event.response;
  if (response.command == "get_new_addresses") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
Ledger.getOperations(1,5);

If user cancels the request:

{
   "command":"get_new_addresses",
   "success":false,
   "message":"Request cancelled by the user"
}

If user grants the request:

{  
  "command":"get_new_addresses",
  "success":true,
  "addresses":{  
    "44'/0'/0'/0/36":"1633oRHv5jPwtZW2bASbS4geWKQd5ENR6F",
    "44'/0'/0'/0/37":"1rvTbWuwEuwDCWbMCgqyKTWUK69jqNXwkv",
    "44'/0'/0'/0/38":"1ewd4XZ8tbNSwGZ6e8fxNKdvFEwaQxVULn",
    "44'/0'/0'/0/39":"1cTyUnyCFfXwShaVMZmL6uMpArEUrKnanj",
    "44'/0'/0'/0/40":"185UpphCFnxowehNarFZnxp9FGFsLJsz6D"
  },
  "account_id":1
}

===

Ledger.sendPayment(address, amount, data)

Request the Chrome app to send a payment of amount BTC to address. If data is set, an additional OP_RETURN output will be added with the data (must be hexadecimal format max 80 bytes).

function callback(event) {
  response = event.response;
  if (response.command == "send_payment") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
Ledger.sendPayment('19QM7ToSsi7N9zsZRdRTLcGZVspXQjQUY5',0.001);

If user cancels the request:

{  
   "command":"send_payment",
   "success":false,
   "message":"Payment cancelled"
}

If user confirms the payment:

{
   "command":"send_payment",
   "success":true,
   "transaction":{
      "amount":100000,
      "fee":10000,
      "hash":"d4b9377db50380aef6c7ba43316ed4c4573c9969cff726a2460febc789c635dc",
      "raw":"01000000025bca73777be5366d8a92226be8da32a4657ff80a80ac5dc33d42bf43e2d63929000000006a473044022004d695816488a4ebd733a45f24fc56bda675c61be65dc4b854f182baa4693e4802206e99b1bd3b40cd06fc7bae67a8c6fcd13f0eb92f3ce02b48e39aca9dbe7e5411012102109fe1ef60221ca9296a43bd3c9306b103c1588c5e413aab39e43e73bb9ce4c5ffffffffd373960e9a2569bf62451eaabc7ddb1bc16fa4088da2a7eb4dd4f626b3f40230000000006a47304402204f6e0e5935226aa235d6a21fe7ea4933447d57668c33d44837e66f4994fe871102201e09c162ffa03af5aa292bc630ab892a60cd07f330032fdb2b1e0bc45f2969c601210234277b06506b5aa8433cf5fa2df36cc252a9b5292a1c3e58a0ce737641ba13eeffffffff02a0860100000000001976a9145c2b5c8ee43ecc2f2b1141ec6924f53fdb0d009d88ac101e3402000000001976a91414baa4000d841e6e4a19a57efa2eeebdff3ce9b788ac00000000"
   }
}

There is no need to push the raw transaction to the Bitcoin network as it has already been done by the Chrome app.

===

Ledger.getXPubKey(path)

Request the extended public key for path.

function callback(event) {
  response = event.response;
  if (response.command == "get_xpubkey") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
Ledger.getXPubKey("44'/0'/0'");

If user cancels the request:

{  
   "command":"get_xpubkey",
   "success":false,
   "message":"Export request cancelled"
}

If user confirms the export request:

{  
   "command":"get_xpubkey",
   "success":true,
   "xpubkey":"xpub6D52jcEfKA4cGeGcVC9pwG37Ju8pUMQrhptw82QVHRSAGBELE5uCee7Qq8RJUqQVyxfJfwbJKYyqyFhc2Xg8cJyN11kRvnAaWcACXP6K0zv"
}

===

Ledger.signP2SH(inputs, scripts, outputs_number, outputs_script, paths)

Request the Ledger Nano to sign a multisignature transaction.

The example below show the expected format of the arguments.

function callback(event) {
  response = event.response;
  if (response.command == "sign_p2sh") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
var inputs = [
  [ "71f97fa2a21486ecd99674a8ae068d92acd2e9db49c199473be39984e6cbe0f6", "00000000" ],
  [ "171e6a969ff196a2cfaaba4780c292e33fc297672a065cc5c5c684727cf9e3ba", "00000001" ]
];
var scripts = [ 
  "52210289b4a3ad52a919abd2bdd6920d8a6879b1e788c38aa76f0440a6f32a9f1996d02103a3393b1439d1693b063482c04bd40142db97bdf139eedd1b51ffb7070a37eac321030b9a409a1e476b0d5d17b804fcdb81cf30f9b99c6f3ae1178206e08bc500639853ae",
  "522102afe2165371442437b86089a17e8d1c26d127e3723b19f568e9c11e326946111521032d139518b16c112d5f1a52157f1468c0b7a570c41673debee8cd2e53eb084df12103b13fe78b0320ceb77795c87ed72069f12edf64169d15f8f9827f0bb4fdbe760f53ae"
];    
var paths = [
  "44'/0'/0'/0/0/0/1",
  "44'/0'/0'/0/0/0/2",
];
var outputs_number = 2
var outputs_script = "40420f00000000001976a91496986c2703c6b311c884bf916d28621bc61e8b7a88acdc0c03000000000017a914ddf0a9f3e0c9822feef702d36dee6c0bd2bf7c6d87"
Ledger.signP2SH(inputs, scripts, outputs_number, outputs_script, paths);

If user cancels the signature request:

{  
   "command":"p2sh",
   "success":false,
   "message":"Signature request declined"
}

If user confirms the signature request:

{  
   "command":"sign_p2sh",
   "success":true,
   "signatures":[  
      "3044022012affaf1b44b4bd365a6ab45f0911ea0825cd621a3adef135f5877af013628ae0220745c106a9823e0b3fdcede7129605420213491b32b40abf2b9934a33614d296301",
      "304502210087f7c10e1f3390558e25dd2ed6c6d4cf6e0f86b2cdd065136b5ed4e3c3c36a3202200ce27357a2b36f7b3b102eeb4eb5b4937c5a84ecc4337f25c46e3704667a930a01"
   ]
}

Nothing is broadcasted on the Bitcoin network.

As it is a P2SH signature, the Ledger Nano won't ask for a second factor confirmation.

===

Ledger.bitid(uri, silent)

Request a BitID login to uri. If silent is true then the Chrome app will not post the signature to the host (you'll have to do it yourself in your JS app).

function callback(event) {
  response = event.response;
  if (response.command == "bitid") {
    console.log(response);
  }
};
Ledger.init({ callback: callback });
Ledger.bitid('bitid://bitid.bitcoin.blue/callback?x=5f38d0fb45b25015&u=1');

If user cancels the authentication request:

{  
   "command":"bitid",
   "success":false,
   "message":"Authentication cancelled"
}

If user confirms the authentication request:

{  
   "command":"bitid",
   "success":true,
   "address":"1M7gUz4NRLBty5WUuNQPNNG5GE2x5t6Wbp",
   "signature":"H5/DFfbXP6tX9bNn/74l/HGC7jhInL5cKCFtLLUQ4nt6C1dB6hepuAlHSI5tG2TsLG6p6ox1qk1EUiMnFikDrEg=",
   "uri":"bitid://bitid.bitcoin.blue/callback?x=5f38d0fb45b25015&u=1"
}

Roadmap

If you would like to see other API calls for your specific needs, please open an issue with a description of what you would like and we'll discuss adding this functionality on our roadmap.