UNPKG

@ledgerhq/hw-app-canton

Version:
119 lines 4.2 kB
import { UserRefusedAddress, UserRefusedOnDevice } from "@ledgerhq/errors"; import BIPPath from "bip32-path"; import { MockCantonDevice } from "./MockDevice"; const CLA = 0xe0; const P1_NON_CONFIRM = 0x00; const P1_CONFIRM = 0x01; const P2 = 0x00; const INS = { GET_VERSION: 0x04, GET_ADDR: 0x05, SIGN: 0x06, }; const STATUS = { OK: 0x9000, USER_CANCEL: 0x6985, }; /** * Canton BOLOS API */ export default class Canton { transport; transportMock; constructor(transport, scrambleKey = "canton_default_scramble_key") { this.transport = transport; this.transportMock = new MockCantonDevice(); transport.decorateAppAPIMethods(this, ["getAddress", "signTransaction"], scrambleKey); } /** * Get a Canton address for a given BIP-32 path. * * @param path a path in BIP-32 format * @param display whether to display the address on the device * @return the address and public key */ async getAddress(path, display = false) { const bipPath = BIPPath.fromString(path).toPathArray(); const serializedPath = this.serializePath(bipPath); const p1 = display ? P1_CONFIRM : P1_NON_CONFIRM; const response = await this.transportMock.send(CLA, INS.GET_ADDR, p1, P2, serializedPath); const responseData = this.handleTransportResponse(response, "address"); // Handle 65-byte uncompressed SECP256R1 public key const publicKey = "0x" + responseData.toString("hex"); const addressHash = this.hashString(publicKey); const address = "canton_" + addressHash.substring(0, 36); return { publicKey, address, }; } /** * Sign a Canton transaction. * * @param path a path in BIP-32 format * @param rawTx the raw transaction to sign * @return the signature */ async signTransaction(path, rawTx) { const bipPath = BIPPath.fromString(path).toPathArray(); const serializedPath = this.serializePath(bipPath); const payload = Buffer.concat([serializedPath, Buffer.from(rawTx, "hex")]); const response = await this.transportMock.send(CLA, INS.SIGN, P1_CONFIRM, P2, payload); const responseData = this.handleTransportResponse(response, "transaction"); const signature = "0x" + responseData.toString("hex"); return signature; } /** * Get the app configuration. * @return the app configuration including version */ async getAppConfiguration() { const [major, minor, patch] = await this.transportMock.send(CLA, INS.GET_VERSION, P1_NON_CONFIRM, P2, Buffer.alloc(0)); return { version: `${major}.${minor}.${patch}`, }; } /** * Helper method to handle transport response and check for errors * @private */ handleTransportResponse(response, errorType) { const statusCode = response.readUInt16BE(response.length - 2); const responseData = response.slice(0, response.length - 2); if (statusCode === STATUS.USER_CANCEL) { if (errorType === "address") { throw new UserRefusedAddress(); } else { throw new UserRefusedOnDevice(); } } return responseData; } /** * Serialize a BIP path to a data buffer for Canton BOLOS * @private */ serializePath(path) { const data = Buffer.alloc(1 + path.length * 4); data.writeUInt8(path.length, 0); // Write path length as first byte path.forEach((segment, index) => { data.writeUInt32BE(segment, 1 + index * 4); // Write each segment as 32-bit integer }); return data; } /** * Simple deterministic hash function for generating mock addresses * @private */ hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } } //# sourceMappingURL=Canton.js.map