Skip to content

Commit f5363e3

Browse files
authored
Merge pull request LedgerHQ#1 from HiddenField/feature/IOHKLE-204-migrate-new-ledgerjs-api
IOHKLE-204: Cardano ADA integration
2 parents 4cf5d06 + bf01f33 commit f5363e3

24 files changed

+2672
-904
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ node_modules
22
.DS_Store
33
*.pem
44
browser/
5-
packages/*/lib/
5+
packages/**/lib/
66
flow-typed/
77
lerna-debug.log
88
lerna-error.log
99
yarn-error.log
10+
*.swp

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ applications. There are implementations for Node and Browser.
1919
| [`@ledgerhq/hw-app-btc`](/packages/hw-app-btc) | [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-app-btc.svg)](https://www.npmjs.com/package/@ledgerhq/hw-app-btc) | Bitcoin Application API |
2020
| [`@ledgerhq/hw-app-xrp`](/packages/hw-app-xrp) | [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-app-xrp.svg)](https://www.npmjs.com/package/@ledgerhq/hw-app-xrp) | Ripple Application API |
2121
| [`@ledgerhq/hw-app-str`](/packages/hw-app-str) | [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-app-str.svg)](https://www.npmjs.com/package/@ledgerhq/hw-app-str) | Stellar Application API |
22+
| [`@ledgerhq/hw-app-ada`](/packages/hw-app-ada) | [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-app-ada.svg)](https://www.npmjs.com/package/@ledgerhq/hw-app-ada) | Cardano ADA Application API |
2223
| **Transports** |
2324
| [`@ledgerhq/hw-transport-node-hid`](/packages/hw-transport-node-hid) | [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-node-hid.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-node-hid) | Node implementation of the communication layer, using `node-hid` (USB) |
2425
| [`@ledgerhq/hw-transport-u2f`](/packages/hw-transport-u2f) | [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-u2f.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-u2f) | Web implementation of the communication layer, using [U2F api](https://github.com/grantila/u2f-api) |

packages/hw-app-ada/.flowconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[ignore]
2+
3+
[include]
4+
5+
[libs]
6+
flow-typed
7+
8+
[lints]
9+
10+
[options]
11+
munge_underscores=true
12+
13+
[strict]

packages/hw-app-ada/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<img src="https://user-images.githubusercontent.com/211411/34776833-6f1ef4da-f618-11e7-8b13-f0697901d6a8.png" height="100" />
2+
3+
## @ledgerhq/hw-app-ada
4+
5+
Library for Ledger Hardware Wallets.
6+
7+
[Github](https://github.com/LedgerHQ/ledgerjs/),
8+
[API Doc](http://ledgerhq.github.io/ledgerjs/),
9+
[Ledger Devs Slack](https://ledger-dev.slack.com/)
10+
11+
### Application
12+
13+
This library is compatible with the [Cardano ADA Ledger Application](https://github.com/HiddenField/ledger-cardano-app).
14+
15+
### Tests
16+
17+
As well as the tests in `@ledgerhq/t@est`, automated end-to-end tests are provided here using [mocha](https://mochajs.org/).
18+
19+
#### Core Tests
20+
21+
Core tests are provided for testing the base functionality of the device.
22+
23+
First, ensure you have a **test** build installed on the device (see ledger app respository for details). Then run:
24+
25+
```shell
26+
yarn run core-test
27+
```
28+
29+
#### API Tests
30+
31+
These test the production API and can be run either on a production build or headlessly for fully-automated testing.
32+
33+
For tests which require user interaction, ensure you have a standard production build of the app and run:
34+
35+
```shell
36+
yarn run api-test
37+
```
38+
39+
For headless tests, ensure you have a **headless** build installed on the device (see ledger app repository for details). Then run:
40+
41+
```shell
42+
yarn run api-test --headless
43+
```

packages/hw-app-ada/package.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "@ledgerhq/hw-app-ada",
3+
"version": "0.1.0",
4+
"description": "Ledger Hardware Wallet Cardano ADA API",
5+
"main": "lib/Ada.js",
6+
"repository": "git+ssh://[email protected]/LedgerHQ/ledgerjs.git",
7+
"keywords": [
8+
"Ledger",
9+
"LedgerWallet",
10+
"ada",
11+
"Cardano",
12+
"SL",
13+
"NanoS",
14+
"Hardware",
15+
"Wallet"
16+
],
17+
"author": "HiddenField Ltd <[email protected]>",
18+
"license": "Apache-2.0",
19+
"dependencies": {
20+
"@ledgerhq/hw-transport": "^4.7.3",
21+
"node-int64": "^0.4.0"
22+
},
23+
"devDependencies": {
24+
"@ledgerhq/hw-transport-node-hid": "^4.6.0",
25+
"chai": "^4.1.2",
26+
"chalk": "^2.3.1",
27+
"flow-bin": "^0.68.0",
28+
"flow-typed": "^2.4.0",
29+
"joi": "^13.1.2",
30+
"mocha": "^5.0.1"
31+
},
32+
"bugs": {
33+
"url": "https://github.com/LedgerHQ/ledgerjs/issues"
34+
},
35+
"homepage": "https://github.com/LedgerHQ/ledgerjs#readme",
36+
"scripts": {
37+
"flow": "flow",
38+
"clean": "rm -rf lib/ flow-typed/",
39+
"build": "cd ../.. && export PATH=$(yarn bin):$PATH && cd - && babel --source-maps -d lib src && flow-copy-source -v src lib",
40+
"watch": "cd ../.. && export PATH=$(yarn bin):$PATH && cd - && babel --watch --source-maps -d lib src & flow-copy-source -w -v src lib",
41+
"clean-test": "rm -rf test/lib",
42+
"build-test": "yarn run clean-test && cd ../.. && export PATH=$(yarn bin):$PATH && cd - && babel --source-maps -d test/lib test/src && flow-copy-source -v test/src test/lib",
43+
"core-test": "yarn run build-test && yarn run flow && mocha --timeout 3500 test/lib/core",
44+
"api-test": "yarn run build-test && yarn run flow && mocha --timeout 15000 test/lib/api"
45+
}
46+
}

packages/hw-app-ada/src/Ada.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/********************************************************************************
2+
* Ledger Node JS API
3+
* (c) 2016-2017 Ledger
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
********************************************************************************/
17+
// @flow
18+
19+
import Int64 from "node-int64";
20+
import type Transport from "@ledgerhq/hw-transport";
21+
import { TransportStatusError } from "@ledgerhq/hw-transport";
22+
23+
const CLA = 0x80;
24+
25+
const INS_GET_PUBLIC_KEY = 0x01;
26+
const INS_SET_TX = 0x02;
27+
const INS_SIGN_TX = 0x03;
28+
const INS_APP_INFO = 0x04;
29+
30+
const P1_FIRST = 0x01;
31+
const P1_NEXT = 0x02;
32+
const P1_LAST = 0x03;
33+
34+
const P2_SINGLE_TX = 0x01;
35+
const P2_MULTI_TX = 0x02;
36+
37+
const MAX_APDU_SIZE = 64;
38+
const OFFSET_CDATA = 5;
39+
const MAX_ADDR_PRINT_LENGTH = 12;
40+
const INDEX_MAX = 0xFFFFFFFF;
41+
42+
const INDEX_NAN = 0x5003;
43+
const INDEX_MAX_EXCEEDED = 0x5302;
44+
45+
/**
46+
* Cardano ADA API
47+
*
48+
* @example
49+
* import Ada from "@ledgerhq/hw-app-ada";
50+
* const ada = new Ada(transport);
51+
*/
52+
export default class Ada {
53+
transport: Transport<*>;
54+
methods: Array<string>;
55+
56+
constructor(transport: Transport<*>) {
57+
this.transport = transport;
58+
this.methods = [ "isConnected", "getWalletRecoveryPassphrase", "getWalletPublicKeyWithIndex", "signTransaction" ];
59+
this.transport.decorateAppAPIMethods(this, this.methods, "ADA");
60+
}
61+
62+
/**
63+
* Checks if the device is connected and if so, returns an object
64+
* containing the app version.
65+
*
66+
* @returns {Promise<{major:number, minor:number, patch:number}>} Result object containing the application version number.
67+
*
68+
* @example
69+
* const { major, minor, patch } = await ada.isConnected();
70+
* console.log(`App version ${major}.${minor}.${patch}`);
71+
*
72+
*/
73+
async isConnected(): Promise<{ major: string, minor: string, patch: string }> {
74+
const response = await this.transport.send(CLA, INS_APP_INFO, 0x00, 0x00);
75+
76+
const [ major, minor, patch ] = response;
77+
return { major, minor, patch };
78+
}
79+
80+
/**
81+
* @description Get the root extended public key of the wallet,
82+
* also known as the wallet recovery passphrase.
83+
* BIP 32 Path M 44' /1815'
84+
* 32 Byte Public Key
85+
* 32 Byte Chain Code
86+
*
87+
* @return {Promise<{ publicKey:string, chainCode:string }>} The result object containing the root wallet public key and chaincode.
88+
*
89+
* @example
90+
* const { publicKey, chainCode } = await ada.getWalletRecoveryPassphrase();
91+
* console.log(publicKey);
92+
* console.log(chainCode);
93+
*
94+
*/
95+
async getWalletRecoveryPassphrase(): Promise<{ publicKey: string, chainCode: string }> {
96+
const response = await this.transport.send(CLA, INS_GET_PUBLIC_KEY, 0x01, 0x00);
97+
98+
const [ publicKeyLength ] = response;
99+
const publicKey = response.slice(1, 1 + publicKeyLength).toString("hex");
100+
const chainCode = response.slice(1 + publicKeyLength, 1 + publicKeyLength + 32).toString("hex");
101+
102+
return { publicKey, chainCode };
103+
}
104+
105+
/**
106+
* @description Get a public key from the specified BIP 32 index.
107+
* The BIP 32 index is from the path at `44'/1815'/0'/[index]`.
108+
*
109+
* @param {number} index The index to retrieve.
110+
* @return {Promise<{ publicKey:string }>} The public key for the given index.
111+
*
112+
* @throws 5201 - Non-hardened index passed in, Index < 0x80000000
113+
* @throws 5202 - Invalid header
114+
* @throws 5003 - Index not a number
115+
*
116+
* @example
117+
* const { publicKey } = await ada.getWalletPublicKeyWithIndex(0xC001CODE);
118+
* console.log(publicKey);
119+
*
120+
*/
121+
async getWalletPublicKeyWithIndex(index: number): Promise<{ publicKey: string }> {
122+
if (isNaN(index)) {
123+
throw new TransportStatusError(INDEX_NAN);
124+
}
125+
126+
const data = Buffer.alloc(4);
127+
data.writeUInt32BE(index, 0);
128+
129+
const response = await this.transport.send(CLA, INS_GET_PUBLIC_KEY, 0x02, 0x00, data);
130+
131+
const [ publicKeyLength ] = response;
132+
const publicKey = response.slice(1, 1 + publicKeyLength).toString("hex");
133+
134+
return { publicKey };
135+
}
136+
137+
/**
138+
* @description Signs a hex encoded transaction with the given indexes.
139+
* The transaction is hased using Blake2b on the Ledger device.
140+
* Then, signed by the private key derived from each of the passed in indexes at
141+
* path 44'/1815'/0'/[index].
142+
*
143+
* @param {string} txHex The transaction to be signed.
144+
* @param {number[]} indexes The indexes of the keys to be used for signing.
145+
* @return {Array.Promise<{ digest:string }>} An array of result objects containing a digest for each of the passed in indexes.
146+
*
147+
* @throws 5001 - Tx > 1024 bytes
148+
* @throws 5301 - Index < 0x80000000
149+
* @throws 5302 - Index > 0xFFFFFFFF
150+
* @throws 5003 - Index not a number
151+
*
152+
* @example
153+
* const transaction = '839F8200D8185826825820E981442C2BE40475BB42193CA35907861D90715854DE6FCBA767B98F1789B51219439AFF9F8282D818584A83581CE7FE8E468D2249F18CD7BF9AEC0D4374B7D3E18609EDE8589F82F7F0A20058208200581C240596B9B63FC010C06FBE92CF6F820587406534795958C411E662DC014443C0688E001A6768CC861B0037699E3EA6D064FFA0';
154+
* const { digest } = await ada.signTransaction(transaction, [0xF005BA11]);
155+
* console.log(`Signed successfully: ${digest}`);
156+
*
157+
*/
158+
async signTransaction(txHex: string, indexes: Array<number>): Promise<Array<{ digest: string }>> {
159+
await this.setTransaction(txHex);
160+
return this.signTransactionWithIndexes(indexes);
161+
}
162+
163+
/**
164+
* Set the transaction.
165+
*
166+
* @param {string} txHex The transaction to be set.
167+
* @return Promise<{ inputs?: string, outputs?: string, txs?: Array<{ address: string, amount: string }> }> The response from the device.
168+
* @private
169+
*/
170+
async setTransaction(txHex: string): Promise<{ inputs?: string, outputs?: string, txs?: Array<{ address: string, amount: string }> }> {
171+
const rawTx = Buffer.from(txHex, "hex");
172+
const chunkSize = MAX_APDU_SIZE - OFFSET_CDATA;
173+
let response = {};
174+
175+
for (let i = 0; i < rawTx.length; i += chunkSize) {
176+
const chunk = rawTx.slice(i, i + chunkSize);
177+
const p2 = rawTx.length < chunkSize ? P2_SINGLE_TX : P2_MULTI_TX;
178+
let p1 = P1_NEXT;
179+
180+
if (i === 0) {
181+
p1 = P1_FIRST;
182+
} else if (i + chunkSize >= rawTx.length) {
183+
p1 = P1_LAST;
184+
}
185+
186+
const res = await this.transport.send(CLA, INS_SET_TX, p1, p2, chunk);
187+
188+
if (res.length > 4) {
189+
const [ inputs, outputs ] = res;
190+
const txs = [];
191+
192+
let offset = 2;
193+
for (let i = 0; i < outputs; i++) {
194+
let address = res.slice(offset, offset + MAX_ADDR_PRINT_LENGTH).toString();
195+
offset += MAX_ADDR_PRINT_LENGTH;
196+
let amount = new Int64(res.readUInt32LE(offset + 4), res.readUInt32LE(offset)).toOctetString();
197+
txs.push({ address, amount });
198+
offset += 8;
199+
}
200+
201+
response = { inputs, outputs, txs };
202+
}
203+
}
204+
205+
return response;
206+
}
207+
208+
/**
209+
* Sign the set transaction with the given indexes.
210+
* Note that setTransaction must be called prior to this being called.
211+
*
212+
* @param {number[]} indexes The indexes of the keys to be used for signing.
213+
* @returns {Array.Promise<Object>} An array of result objects containing a digest for each of the passed in indexes.
214+
* @private
215+
*/
216+
async signTransactionWithIndexes(indexes: Array<number>): Promise<Array<{ digest: string }>> {
217+
let response = [];
218+
219+
for (let i = 0; i < indexes.length; i++) {
220+
if (isNaN(indexes[i])) {
221+
throw new TransportStatusError(INDEX_NAN);
222+
}
223+
224+
if (indexes[i] > INDEX_MAX) {
225+
throw new TransportStatusError(INDEX_MAX_EXCEEDED);
226+
}
227+
228+
const data = Buffer.alloc(4);
229+
data.writeUInt32BE(indexes[i], 0);
230+
231+
const res = await this.transport.send(CLA, INS_SIGN_TX, 0x00, 0x00, data);
232+
const digest = res.slice(0, res.length - 2).toString("hex");
233+
234+
response.push({ digest });
235+
}
236+
237+
return response;
238+
}
239+
}

0 commit comments

Comments
 (0)