Skip to content

Commit fb65527

Browse files
authored
Add support for multibyte versions + tests + docs (#23)
* correst testing script * add support for multibyte versions + tests + docs * fix bip32 tests
1 parent 979258b commit fb65527

File tree

12 files changed

+414
-67
lines changed

12 files changed

+414
-67
lines changed

bun.lockb

4.4 KB
Binary file not shown.

package.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@
2121
"main": "./dist/index.js",
2222
"types": "./dist/index.d.ts",
2323
"files": [
24-
"dist/**"
24+
"dist/**",
25+
"LICENSE",
26+
"package.json"
2527
],
2628
"scripts": {
2729
"build": "tsc",
2830
"format": "FORCE_COLOR=1 prettier --write . && sort-package-json",
2931
"format:check": "FORCE_COLOR=1 prettier --check .",
3032
"lint": "eslint .",
3133
"lint:fix": "eslint --fix .",
32-
"supported": "ts-node src/cmd/cli.ts supported",
33-
"test:integration": "yarn build && jest -t 'Integration'",
34-
"test:key-derivation": "yarn build && jest -t 'KeyDerivation'",
34+
"test": "yarn build && ts-jest",
3535
"upgrade": "bunx npm-check-updates -i"
3636
},
3737
"dependencies": {
@@ -46,14 +46,15 @@
4646
"@typescript-eslint/parser": "^7.8.0",
4747
"eslint": "^9.2.0",
4848
"eslint-config-prettier": "^9.1.0",
49-
"eslint-config-standard-with-typescript": "^34.0.1",
49+
"eslint-config-standard-with-typescript": "^39.0.0",
5050
"eslint-plugin-import": "^2.29.1",
51-
"eslint-plugin-n": "^17.6.0",
51+
"eslint-plugin-n": "^17.7.0",
5252
"eslint-plugin-promise": "^6.1.1",
5353
"eslint-plugin-tsdoc": "^0.2.17",
5454
"eslint-plugin-unused-imports": "^3.2.0",
5555
"prettier": "^3.2.5",
5656
"ts-jest": "^29.1.2",
57+
"ts-node": "^10.9.2",
5758
"typescript": "^5.4.5"
5859
},
5960
"volta": {
@@ -62,5 +63,9 @@
6263
},
6364
"publishConfig": {
6465
"access": "public"
65-
}
66+
},
67+
"moduleDirectories": [
68+
"node_modules",
69+
"dist"
70+
]
6671
}

src/app.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('BaseApp', () => {
4343
})
4444

4545
describe('getVersion', () => {
46-
it('should retrieve version information', async () => {
46+
it('should retrieve version information (5 bytes)', async () => {
4747
const responseBuffer = Buffer.concat([
4848
Buffer.from([0, 1, 2, 3, 0]), // Version information
4949
Buffer.from([0x90, 0x00]), // Status code for no errors (0x9000)
@@ -63,6 +63,66 @@ describe('BaseApp', () => {
6363
})
6464
})
6565

66+
it('should retrieve version information (9 bytes)', async () => {
67+
const responseBuffer = Buffer.concat([
68+
Buffer.from([1, 2, 3, 4, 0, 0, 0, 0, 0]), // Version information
69+
Buffer.from([0x90, 0x00]), // Status code for no errors (0x9000)
70+
])
71+
72+
const transport = new MockTransport(responseBuffer)
73+
const app = new BaseApp(transport, params)
74+
const version = await app.getVersion()
75+
76+
expect(version).toEqual({
77+
major: 2,
78+
minor: 3,
79+
patch: 4,
80+
deviceLocked: false,
81+
targetId: '00000000',
82+
testMode: true,
83+
})
84+
})
85+
86+
it('should retrieve version information (8 bytes)', async () => {
87+
const responseBuffer = Buffer.concat([
88+
Buffer.from([1, 0, 7, 0, 8, 0, 9, 1]), // Version information
89+
Buffer.from([0x90, 0x00]), // Status code for no errors (0x9000)
90+
])
91+
92+
const transport = new MockTransport(responseBuffer)
93+
const app = new BaseApp(transport, params)
94+
const version = await app.getVersion()
95+
96+
expect(version).toEqual({
97+
major: 7,
98+
minor: 8,
99+
patch: 9,
100+
deviceLocked: true,
101+
targetId: '',
102+
testMode: true,
103+
})
104+
})
105+
106+
it('should retrieve version information (12 bytes)', async () => {
107+
const responseBuffer = Buffer.concat([
108+
Buffer.from([1, 1, 5, 0, 6, 0, 7, 0, 0, 0xa, 0xb, 0xc]), // Version information
109+
Buffer.from([0x90, 0x00]), // Status code for no errors (0x9000)
110+
])
111+
112+
const transport = new MockTransport(responseBuffer)
113+
const app = new BaseApp(transport, params)
114+
const version = await app.getVersion()
115+
116+
expect(version).toEqual({
117+
major: 261,
118+
minor: 6,
119+
patch: 7,
120+
deviceLocked: false,
121+
targetId: '000a0b0c',
122+
testMode: true,
123+
})
124+
})
125+
66126
it('should handle missing data', async () => {
67127
const responseBuffer = Buffer.concat([
68128
Buffer.from([0, 1, 2, 3]), // Version information
@@ -72,7 +132,7 @@ describe('BaseApp', () => {
72132
const transport = new MockTransport(responseBuffer)
73133
const app = new BaseApp(transport, params)
74134

75-
await expect(app.getVersion()).rejects.toEqual(new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length'))
135+
await expect(app.getVersion()).rejects.toEqual(new ResponseError(LedgerError.UnknownError, 'Invalid response length'))
76136
})
77137

78138
it('should handle errors correctly', async () => {

src/app.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import {
2929
type ResponseVersion,
3030
} from './types'
3131

32+
/**
33+
* Base class for interacting with a Ledger device.
34+
*/
3235
export default class BaseApp {
3336
readonly transport: Transport
3437
readonly CLA: number
@@ -37,6 +40,11 @@ export default class BaseApp {
3740
readonly REQUIRED_PATH_LENGTHS?: number[]
3841
readonly CHUNK_SIZE: number
3942

43+
/**
44+
* Constructs a new BaseApp instance.
45+
* @param transport - The transport mechanism to communicate with the device.
46+
* @param params - The constructor parameters.
47+
*/
4048
constructor(transport: Transport, params: ConstructorParams) {
4149
this.transport = transport
4250
this.CLA = params.cla
@@ -46,6 +54,11 @@ export default class BaseApp {
4654
this.REQUIRED_PATH_LENGTHS = params.acceptedPathLengths
4755
}
4856

57+
/**
58+
* Serializes a derivation path into a buffer.
59+
* @param path - The derivation path in string format.
60+
* @returns A buffer representing the serialized path.
61+
*/
4962
serializePath(path: string): Buffer {
5063
return serializePath(path, this.REQUIRED_PATH_LENGTHS)
5164
}
@@ -54,7 +67,7 @@ export default class BaseApp {
5467
* Prepares chunks of data to be sent to the device.
5568
* @param path - The derivation path.
5669
* @param message - The message to be sent.
57-
* @returns An array of buffers ready to be sent.
70+
* @returns An array of buffers that are ready to be sent.
5871
*/
5972
prepareChunks(path: string, message: Buffer): Buffer[] {
6073
const chunks = []
@@ -114,20 +127,43 @@ export default class BaseApp {
114127
const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_VERSION, 0, 0)
115128
const response = processResponse(responseBuffer)
116129

117-
const testMode = response.readBytes(1).readUInt8() !== 0
118-
const major = response.readBytes(1).readUInt8()
119-
const minor = response.readBytes(1).readUInt8()
120-
const patch = response.readBytes(1).readUInt8()
130+
// valid options are
131+
// test mode: 1 byte
132+
// major, minor, patch: 3 byte total
133+
// device locked: 1 byte
134+
// targetId: 4 bytes
135+
// total: 5 or 9 bytes
136+
// -----
137+
// test mode: 1 byte
138+
// major, minor, patch: 6 byte total
139+
// device locked: 1 byte
140+
// targetId: 4 bytes
141+
// total: 8 or 12 bytes
142+
143+
let testMode
144+
let major, minor, patch
145+
146+
if (response.length() === 5 || response.length() === 9) {
147+
testMode = response.readBytes(1).readUInt8() !== 0
148+
major = response.readBytes(1).readUInt8()
149+
minor = response.readBytes(1).readUInt8()
150+
patch = response.readBytes(1).readUInt8()
151+
} else if (response.length() === 8 || response.length() === 12) {
152+
testMode = response.readBytes(1).readUInt8() !== 0
153+
major = response.readBytes(2).readUInt16BE()
154+
minor = response.readBytes(2).readUInt16BE()
155+
patch = response.readBytes(2).readUInt16BE()
156+
} else {
157+
throw new ResponseError(LedgerError.TechnicalProblem, 'Invalid response length')
158+
}
121159

122160
const deviceLocked = response.readBytes(1).readUInt8() === 1
123161

124162
let targetId = ''
125163
if (response.length() >= 4) {
126-
targetId = response.readBytes(4).readUInt32BE().toString(16)
164+
targetId = response.readBytes(4).readUInt32BE().toString(16).padStart(8, '0')
127165
}
128166

129-
// FIXME: Add support for devices with multibyte version numbers
130-
131167
return {
132168
testMode,
133169
major,
@@ -140,7 +176,6 @@ export default class BaseApp {
140176
throw processErrorResponse(error)
141177
}
142178
}
143-
144179
/**
145180
* Retrieves application information from the device.
146181
* @returns A promise that resolves to the application information.

0 commit comments

Comments
 (0)