Skip to content

Commit 2f7a5c3

Browse files
authored
Merge pull request #2051 from aeternity/new-ledger-app
Experimental support of Ledger app v1.0.0
2 parents 5c4f429 + 0116d74 commit 2f7a5c3

File tree

3 files changed

+75
-37
lines changed

3 files changed

+75
-37
lines changed

examples/browser/aepp/src/components/ConnectLedger.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export default {
8585
transport = await (isBle ? TransportWebBleAndroidFix : TransportWebUsb).create();
8686
transport.on('disconnect', () => this.reset());
8787
const factory = new AccountLedgerFactory(transport);
88+
factory._enableExperimentalLedgerAppSupport = true;
8889
await factory.ensureReady();
8990
this.accountFactory = factory;
9091
this.status = '';

src/account/LedgerFactory.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,22 @@ export default class AccountLedgerFactory extends AccountBaseFactory {
2323

2424
#ensureReadyPromise?: Promise<void>;
2525

26+
// TODO: remove after release Ledger app v1.0.0
27+
_enableExperimentalLedgerAppSupport = false;
28+
2629
/**
2730
* It throws an exception if Aeternity app on Ledger has an incompatible version, not opened or
2831
* not installed.
2932
*/
3033
async ensureReady(): Promise<void> {
3134
const { version } = await this.#getAppConfiguration();
32-
const args = [version, '0.4.4', '0.5.0'] as const;
33-
if (!semverSatisfies(...args))
34-
throw new UnsupportedVersionError('Aeternity app on Ledger', ...args);
35+
const oldApp = [version, '0.4.4', '0.5.0'] as const;
36+
const newApp = [version, '1.0.0', '2.0.0'] as const;
37+
if (
38+
!semverSatisfies(...oldApp) &&
39+
(!this._enableExperimentalLedgerAppSupport || !semverSatisfies(...newApp))
40+
)
41+
throw new UnsupportedVersionError('Aeternity app on Ledger', ...oldApp);
3542
this.#ensureReadyPromise = Promise.resolve();
3643
}
3744

@@ -41,9 +48,10 @@ export default class AccountLedgerFactory extends AccountBaseFactory {
4148
}
4249

4350
async #getAppConfiguration(): Promise<AppConfiguration> {
44-
const response = await this.transport.send(CLA, GET_APP_CONFIGURATION, 0x00, 0x00);
51+
let response = await this.transport.send(CLA, GET_APP_CONFIGURATION, 0x00, 0x00);
52+
if (response.length === 6) response = response.subarray(1);
4553
return {
46-
version: [response[1], response[2], response[3]].join('.'),
54+
version: [response[0], response[1], response[2]].join('.'),
4755
};
4856
}
4957

test/unit/ledger.ts

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { describe, it } from 'mocha';
1+
import { createInterface } from 'node:readline/promises';
2+
import { describe, it, before, after } from 'mocha';
23
import { expect } from 'chai';
34
import '..';
45
import {
@@ -10,7 +11,7 @@ import type Transport from '@ledgerhq/hw-transport';
1011
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-singleton';
1112
import {
1213
AccountLedger,
13-
AccountLedgerFactory,
14+
AccountLedgerFactory as AccountLedgerFactoryOriginal,
1415
buildTx,
1516
Node,
1617
Tag,
@@ -26,38 +27,56 @@ import { indent } from '../utils';
2627
const compareWithRealDevice = false; // switch to true for manual testing
2728
// ledger should be initialized with mnemonic:
2829
// eye quarter chapter suit cruel scrub verify stuff volume control learn dust
29-
let recordStore: RecordStore;
30-
let expectedRecordStore = '';
31-
32-
async function initTransport(s: string, ignoreRealDevice = false): Promise<Transport> {
33-
expectedRecordStore = s;
34-
if (compareWithRealDevice && !ignoreRealDevice) {
35-
const t = await TransportNodeHid.create();
36-
recordStore = new RecordStore();
37-
const TransportRecorder = createTransportRecorder(t, recordStore);
38-
return new TransportRecorder(t);
30+
31+
async function initTransport(
32+
expectedRecordStore: string,
33+
ignoreRealDevice = false,
34+
): Promise<Transport> {
35+
if (!compareWithRealDevice || ignoreRealDevice) {
36+
return openTransportReplayer(RecordStore.fromString(expectedRecordStore));
3937
}
40-
recordStore = RecordStore.fromString(expectedRecordStore);
41-
return openTransportReplayer(recordStore);
42-
}
4338

44-
afterEach(async () => {
45-
if (compareWithRealDevice) {
39+
// TODO: remove after solving https://github.com/LedgerHQ/ledger-live/issues/9462
40+
const Transport =
41+
'default' in TransportNodeHid
42+
? (TransportNodeHid.default as typeof TransportNodeHid)
43+
: TransportNodeHid;
44+
const t = await Transport.create();
45+
const recordStore = new RecordStore();
46+
const TransportRecorder = createTransportRecorder(t, recordStore);
47+
after(() => {
4648
expect(recordStore.toString().trim()).to.equal(expectedRecordStore);
47-
}
48-
expectedRecordStore = '';
49-
});
49+
});
50+
return new TransportRecorder(t);
51+
}
5052

51-
describe('Ledger HW', function () {
53+
function genLedgerTests(this: Mocha.Suite, isNewApp = false): void {
5254
this.timeout(compareWithRealDevice ? 60000 : 300);
5355

56+
before(async () => {
57+
if (!compareWithRealDevice) return;
58+
const rl = createInterface({
59+
input: process.stdin,
60+
output: process.stdout,
61+
});
62+
await rl.question(`Open aeternity@${isNewApp ? '1.0.0' : '0.4.4'} on Ledger and press enter`);
63+
rl.close();
64+
});
65+
66+
class AccountLedgerFactory extends AccountLedgerFactoryOriginal {
67+
constructor(transport: Transport) {
68+
super(transport);
69+
this._enableExperimentalLedgerAppSupport = isNewApp;
70+
}
71+
}
72+
5473
describe('factory', () => {
5574
it('gets app version', async () => {
5675
const transport = await initTransport(indent`
5776
=> e006000000
58-
<= 000004049000`);
77+
<= ${isNewApp ? '0100009000' : '000004049000'}`);
5978
const factory = new AccountLedgerFactory(transport);
60-
expect((await factory.getAppConfiguration()).version).to.equal('0.4.4');
79+
expect((await factory.getAppConfiguration()).version).to.equal(isNewApp ? '1.0.0' : '0.4.4');
6180
});
6281

6382
it('ensures that app version is compatible', async () => {
@@ -68,6 +87,7 @@ describe('Ledger HW', function () {
6887
true,
6988
);
7089
const factory = new AccountLedgerFactory(transport);
90+
factory._enableExperimentalLedgerAppSupport = false;
7191
await expect(factory.getAddress(42)).to.be.rejectedWith(
7292
'Unsupported Aeternity app on Ledger version 1.4.4. Supported: >= 0.4.4 < 0.5.0',
7393
);
@@ -76,7 +96,7 @@ describe('Ledger HW', function () {
7696
it('gets address', async () => {
7797
const transport = await initTransport(indent`
7898
=> e006000000
79-
<= 000004049000
99+
<= ${isNewApp ? '0100009000' : '000004049000'}
80100
=> e0020000040000002a
81101
<= 35616b5f3248746565756a614a7a75744b65465a69416d59547a636167536f5245725358704246563137397859677154347465616b769000`);
82102
const factory = new AccountLedgerFactory(transport);
@@ -88,7 +108,7 @@ describe('Ledger HW', function () {
88108
it('gets address with verification', async () => {
89109
const transport = await initTransport(indent`
90110
=> e006000000
91-
<= 000004049000
111+
<= ${isNewApp ? '0100009000' : '000004049000'}
92112
=> e0020100040000002a
93113
<= 35616b5f3248746565756a614a7a75744b65465a69416d59547a636167536f5245725358704246563137397859677154347465616b769000`);
94114
const factory = new AccountLedgerFactory(transport);
@@ -100,7 +120,7 @@ describe('Ledger HW', function () {
100120
it('gets address with verification rejected', async () => {
101121
const transport = await initTransport(indent`
102122
=> e006000000
103-
<= 000004049000
123+
<= ${isNewApp ? '0100009000' : '000004049000'}
104124
=> e0020100040000002a
105125
<= 6985`);
106126
const factory = new AccountLedgerFactory(transport);
@@ -112,7 +132,7 @@ describe('Ledger HW', function () {
112132
it('initializes an account', async () => {
113133
const transport = await initTransport(indent`
114134
=> e006000000
115-
<= 000004049000
135+
<= ${isNewApp ? '0100009000' : '000004049000'}
116136
=> e0020000040000002a
117137
<= 35616b5f3248746565756a614a7a75744b65465a69416d59547a636167536f5245725358704246563137397859677154347465616b769000`);
118138
const factory = new AccountLedgerFactory(transport);
@@ -147,7 +167,7 @@ describe('Ledger HW', function () {
147167
it('discovers accounts', async () => {
148168
const transport = await initTransport(indent`
149169
=> e006000000
150-
<= 000004049000
170+
<= ${isNewApp ? '0100009000' : '000004049000'}
151171
=> e00200000400000000
152172
<= 35616b5f327377684c6b674250656541447856544156434a6e5a4c59354e5a744346694d39334a787345614d754335396575754652519000
153173
=> e00200000400000001
@@ -166,7 +186,7 @@ describe('Ledger HW', function () {
166186
it('discovers accounts on unused ledger', async () => {
167187
const transport = await initTransport(indent`
168188
=> e006000000
169-
<= 000004049000
189+
<= ${isNewApp ? '0100009000' : '000004049000'}
170190
=> e00200000400000000
171191
<= 35616b5f327377684c6b674250656541447856544156434a6e5a4c59354e5a744346694d39334a787345614d754335396575754652519000`);
172192
const node = new NodeMock();
@@ -179,7 +199,7 @@ describe('Ledger HW', function () {
179199
describe('account', () => {
180200
const address = 'ak_2swhLkgBPeeADxVTAVCJnZLY5NZtCFiM93JxsEaMuC59euuFRQ';
181201
it('fails on calling raw signing', async () => {
182-
const transport = await initTransport('\n');
202+
const transport = await initTransport('');
183203
const account = new AccountLedger(transport, 0, address);
184204
await expect(account.unsafeSign()).to.be.rejectedWith('RAW signing using Ledger HW');
185205
});
@@ -224,11 +244,12 @@ describe('Ledger HW', function () {
224244
it('signs message', async () => {
225245
const transport = await initTransport(indent`
226246
=> e00800002f0000000000000027746573742d6d6573736167652c746573742d6d6573736167652c746573742d6d6573736167652c
227-
<= 78397e186058f278835b8e3e866960e4418dc1e9f00b3a2423f57c16021c88720119ebb3373a136112caa1c9ff63870092064659eb2c641dd67767f15c80350c9000`);
247+
<= ${isNewApp ? '78397e186058f278835b8e3e866960e4418dc1e9f00b3a2423f57c16021c88720119ebb3373a136112caa1c9ff63870092064659eb2c641dd67767f15c80350c9000' : '63a9410fa235e4b1f0204cc4d36322e666662da5873b399b076961eced2907f502cd3f91b95bbcfd8e235e194888d469bb15ab4382705aa887c2e0c4e6cb1a0b9000'}`);
228248
const account = new AccountLedger(transport, 0, address);
229249
const signature = await account.signMessage(message);
230250
expect(signature).to.be.an.instanceOf(Uint8Array);
231-
expect(verifyMessage(message, signature, address)).to.equal(true);
251+
// FIXME: correct signature after releasing https://github.com/LedgerHQ/app-aeternity/pull/13
252+
expect(verifyMessage(message, signature, address)).to.equal(isNewApp);
232253
});
233254

234255
it('signs message rejected', async () => {
@@ -241,4 +262,12 @@ describe('Ledger HW', function () {
241262
);
242263
});
243264
});
265+
}
266+
267+
describe('Ledger HW v0.4.4', function () {
268+
genLedgerTests.call(this, false);
269+
});
270+
271+
describe('Ledger HW v1.0.0', function () {
272+
genLedgerTests.call(this, true);
244273
});

0 commit comments

Comments
 (0)