@ledgerhq/hw-app-canton
Version:
Ledger Hardware Wallet Canton Application API
119 lines • 4.2 kB
JavaScript
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