hw-app-kda
Version:
Ledger Hardware Wallet Kadena Application API
157 lines (143 loc) • 4.61 kB
text/typescript
/********************************************************************************
* Ledger Node JS API
* (c) 2016-2017 Ledger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
********************************************************************************/
import type Transport from "@ledgerhq/hw-transport";
export type GetPublicKeyResult = {
publicKey: string;
};
export type SignHashResult = {
signature: string;
};
export type GetVersionResult = {
major: number;
minor: number;
patch: number;
};
/**
* Kadena API
*
* @example
* import Kadena from "hw-app-kda";
* const kda = new Kadena(transport)
*/
export default class Kadena {
transport: Transport;
constructor(transport: Transport) {
this.transport = transport;
transport.decorateAppAPIMethods(
this,
["menu", "getPublicKey", "signHash", "getVersion"],
"KDA"
);
}
// This does not yet work, requires an SDK bug to be fixed.
// The send function adds a single byte representing the size of the payload
// in bytes, but the rust SDK doesn't handle it.
async getPublicKey(
path: string,
): Promise<GetPublicKeyResult> {
const cla = 0x00;
const ins = 0x02;
const p1 = 0;
const p2 = 0;
const payload = buildBip32KeyPayload(path);
console.log("getPublicKey payload", payload.toString("hex")); // TODO remove
const response = await this.transport.send(cla, ins, p1, p2, payload);
console.log("getPublicKey response", response.toString("hex")); // TODO remove
const responseSize = response[0];
const publicKey = response.slice(1, 1 + responseSize);
const res: GetPublicKeyResult = {
publicKey: publicKey.toString("hex"),
};
return res;
}
async signHash(
path: string,
hash: string,
): Promise<SignHashResult> {
const paths = splitPath(path);
const cla = 0x00;
const ins = 0x03;
const p1 = 0;
const p2 = 0;
// Hash payload is the byte length as uint32le followed by the bytes
const rawHash = Buffer.from(hash, "hex");
const hashSize = Buffer.alloc(4);
hashSize.writeUInt32LE(rawHash.length, 0);
// Bip32key payload same as getPublicKey
const bip32KeyPayload = buildBip32KeyPayload(path);
// These are just squashed together
const payload = Buffer.concat([hashSize, rawHash, bip32KeyPayload])
console.log("signHash payload", payload.toString("hex")); // TODO remove
// TODO batch this since the payload length can be uint32le.max long
const response = await this.transport.send(cla, ins, p1, p2, payload);
console.log("signHash response", response.toString("hex")); // TODO remove
// TODO check this
const signature = response.toString("hex");
return {
signature,
};
}
async menu(): Promise<string> {
const cla = 0x00;
const ins = 0x04;
const payload = await this.transport.send(cla, ins, 0, 0);
return payload.toString("hex");
}
// TODO this doesn't exist yet
async getVersion(): Promise<GetVersionResult> {
const [major, minor, patch] = await this.transport.send(
0x80,
0x00,
0x00,
0x00,
Buffer.alloc(0)
);
return {
major,
minor,
patch,
};
}
}
function buildBip32KeyPayload(path: string): Buffer {
const paths = splitPath(path);
// Bip32Key payload is:
// 1 byte with number of elements in u32 array path
// Followed by the u32 array itself
const payload = Buffer.alloc(1 + paths.length * 4);
payload[0] = paths.length
paths.forEach((element, index) => {
payload.writeUInt32LE(element, 1 + 4 * index);
});
return payload
}
// TODO use bip32-path library
function splitPath(path: string): number[] {
const result: number[] = [];
const components = path.split("/");
components.forEach((element) => {
let number = parseInt(element, 10);
if (isNaN(number)) {
return; // FIXME shouldn't it throws instead?
}
if (element.length > 1 && element[element.length - 1] === "'") {
number += 0x80000000;
}
result.push(number);
});
return result;
}