@ledgerhq/hw-app-str
Version:
Ledger Hardware Wallet Stellar Application API
199 lines • 8.21 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const bip32_path_1 = __importDefault(require("bip32-path"));
const errors_1 = require("./errors");
const CLA = 0xe0;
const P1_FIRST = 0x00;
const P1_MORE = 0x80;
const P2_LAST = 0x00;
const P2_MORE = 0x80;
const P2_NON_CONFIRM = 0x00; // for getPublicKey
const P2_CONFIRM = 0x01; // for getPublicKey
const INS_GET_PK = 0x02;
const INS_SIGN_TX = 0x04;
const INS_GET_CONF = 0x06;
const INS_SIGN_HASH = 0x08;
const INS_SIGN_SOROBAN_AUTHORIZATION = 0x0a;
const APDU_MAX_PAYLOAD = 255;
const SW_DENY = 0x6985;
const SW_HASH_SIGNING_MODE_NOT_ENABLED = 0x6c66;
const SW_DATA_TOO_LARGE = 0xb004;
const SW_DATA_PARSING_FAIL = 0xb005;
/**
* Stellar API
*
* @param transport a transport for sending commands to a device
* @param scrambleKey a scramble key
*
* @example
* import Str from "@ledgerhq/hw-app-str";
* const str = new Str(transport)
*/
class Str {
transport;
constructor(transport, scrambleKey = "l0v") {
this.transport = transport;
transport.decorateAppAPIMethods(this, [
"getAppConfiguration",
"getPublicKey",
"signTransaction",
"signSorobanAuthorization",
"signHash",
], scrambleKey);
}
/**
* Get Stellar application configuration.
*
* @returns an object with the application configuration, including the version,
* whether hash signing is enabled, and the maximum data size in bytes that the device can sign.
* @example
* str.getAppConfiguration().then(o => o.version)
*/
async getAppConfiguration() {
const resp = await this.sendToDevice(INS_GET_CONF, Buffer.alloc(0));
const [hashSigningEnabled, major, minor, patch, maxDataSizeHi, maxDataSizeLo] = resp;
return {
hashSigningEnabled: hashSigningEnabled === 0x01,
version: `${major}.${minor}.${patch}`,
maxDataSize: resp.length > 4 ? (maxDataSizeHi << 8) | maxDataSizeLo : undefined, // For compatibility with older app, let's remove this in the future
};
}
/**
* Get Stellar raw public key for a given BIP 32 path.
*
* @param path a path in BIP 32 format
* @param display if true, the device will ask the user to confirm the address on the device, if false, it will return the raw public key directly
* @return an object with the raw ed25519 public key.
* If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey}
* @example
* str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey)
*/
async getPublicKey(path, display = false) {
const pathBuffer = pathToBuffer(path);
const p2 = display ? P2_CONFIRM : P2_NON_CONFIRM;
try {
const data = await this.transport.send(CLA, INS_GET_PK, P1_FIRST, p2, pathBuffer);
return { rawPublicKey: data.slice(0, -2) };
}
catch (e) {
throw remapErrors(e);
}
}
/**
* Sign a Stellar transaction.
*
* @param path a path in BIP 32 format
* @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign
* @return an object with the signature
* @example
* str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature)
*/
async signTransaction(path, transaction) {
const pathBuffer = pathToBuffer(path);
const payload = Buffer.concat([pathBuffer, transaction]);
const resp = await this.sendToDevice(INS_SIGN_TX, payload);
return { signature: resp };
}
/**
* Sign a Stellar Soroban authorization.
*
* @param path a path in BIP 32 format
* @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign
* @return an object with the signature
* @example
* str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature)
*/
async signSorobanAuthorization(path, hashIdPreimage) {
const pathBuffer = pathToBuffer(path);
const payload = Buffer.concat([pathBuffer, hashIdPreimage]);
const resp = await this.sendToDevice(INS_SIGN_SOROBAN_AUTHORIZATION, payload);
return { signature: resp };
}
/**
* Sign a hash.
*
* @param path a path in BIP 32 format
* @param hash the hash to sign
* @return an object with the signature
* @example
* str.signHash("44'/148'/0'", hash).then(o => o.signature)
*/
async signHash(path, hash) {
const pathBuffer = pathToBuffer(path);
const payload = Buffer.concat([pathBuffer, hash]);
const resp = await this.sendToDevice(INS_SIGN_HASH, payload);
return { signature: resp };
}
async sendToDevice(instruction, payload) {
let response = Buffer.alloc(0);
let remaining = payload.length;
// eslint-disable-next-line no-constant-condition
while (true) {
const chunkSize = remaining > APDU_MAX_PAYLOAD ? APDU_MAX_PAYLOAD : remaining;
const p1 = remaining === payload.length ? P1_FIRST : P1_MORE;
const p2 = remaining - chunkSize === 0 ? P2_LAST : P2_MORE;
const chunk = payload.slice(payload.length - remaining, payload.length - remaining + chunkSize);
response = await this.transport.send(CLA, instruction, p1, p2, chunk).catch(e => {
throw remapErrors(e);
});
remaining -= chunkSize;
if (remaining === 0) {
break;
}
}
return response.slice(0, -2);
}
}
exports.default = Str;
const remapErrors = e => {
if (e) {
switch (e.statusCode) {
case SW_DENY:
return new errors_1.StellarUserRefusedError("User refused the request", undefined, { cause: e });
case SW_DATA_PARSING_FAIL:
return new errors_1.StellarDataParsingFailedError("Unable to parse the provided data", undefined, {
cause: e,
});
case SW_HASH_SIGNING_MODE_NOT_ENABLED:
return new errors_1.StellarHashSigningNotEnabledError("Hash signing not allowed. Have you enabled it in the app settings?", undefined, { cause: e });
case SW_DATA_TOO_LARGE:
return new errors_1.StellarDataTooLargeError("The provided data is too large for the device to process", undefined, { cause: e });
}
}
return e;
};
const pathToBuffer = (originalPath) => {
const path = originalPath
.split("/")
.map(value => (value.endsWith("'") || value.endsWith("h") ? value : `${value}'`))
.join("/");
const pathNums = bip32_path_1.default.fromString(path).toPathArray();
return serializePath(pathNums);
};
const serializePath = (path) => {
const buf = Buffer.alloc(1 + path.length * 4);
buf.writeUInt8(path.length, 0);
for (const [i, num] of path.entries()) {
buf.writeUInt32BE(num, 1 + i * 4);
}
return buf;
};
__exportStar(require("./errors"), exports);
//# sourceMappingURL=Str.js.map
;