UNPKG

@siacentral/ledgerjs-sia

Version:
230 lines 9.76 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const hw_transport_webhid_1 = __importDefault(require("@ledgerhq/hw-transport-webhid")); const hw_transport_web_ble_1 = __importDefault(require("@ledgerhq/hw-transport-web-ble")); const buffer_1 = require("buffer"); const CLA = 0xe0; const APP_NAME = 'Sia'; const INS_OPEN_APP = 0xd8; const INS_GET_VERSION = 0x01; const INS_GET_PUBLIC_KEY = 0x02; const INS_SIGN_HASH = 0x04; const INS_CALC_V2TXN_HASH = 0x10; const P1_FIRST = 0x00; const P1_MORE = 0x80; const P2_DISPLAY_ADDRESS = 0x00; const P2_DISPLAY_PUBKEY = 0x01; const P2_SIGN_HASH = 0x01; function uint32ToBuffer(val) { const buf = buffer_1.Buffer.alloc(4); buf.writeUInt32LE(val, 0); return buf; } function bytesToHex(bytes) { return bytes.reduce((v, b) => v + ('0' + b.toString(16)).slice(-2), ''); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Sia * * @example * import Sia from '@siacentral/ledgerjs-sia'; * const sia = new Sia(transport) */ class Sia { constructor(transport, scrambleKey = 'Sia') { this.transport = transport; transport.decorateAppAPIMethods(this, [ 'signV2Transaction', 'getPublicKey', 'getAddress', 'signHash', ], scrambleKey); } /** * open connects a transport, ensures the Sia app is running, and returns a * ready Sia instance. If the app is not already open it is launched from the * dashboard; the device disconnects and reconnects when an app opens, so the * transport is recreated via the provided factory until the app is ready. * @param createTransport {TransportFactory} creates a transport, e.g. () => TransportWebHID.create() * @param scrambleKey {string} the scramble key for the Sia app * @returns {Sia} a Sia instance bound to a transport with the Sia app open */ static async open(createTransport, scrambleKey = 'Sia') { let sia = new Sia(await createTransport(), scrambleKey); // if the Sia app is already running, return immediately try { await sia.getVersion(); return sia; } catch { // the app is not open yet } // open the Sia app; the device disconnects and reconnects try { await sia.openApp(); } catch { // the device disconnects as the app opens } await sia.close().catch(() => undefined); // the device reconnects when an app is opened; poll until the Sia app is ready for (let i = 0; i < 10; i++) { await delay(500); try { sia = new Sia(await createTransport(), scrambleKey); } catch { continue; } try { await sia.getVersion(); return sia; } catch { await sia.close().catch(() => undefined); } } throw new Error('Sia app did not become ready'); } /** * connectWebHID connects to a Ledger over WebHID (USB), launching the Sia * app from the dashboard if it isn't already open, and returns a ready Sia * instance. * @param scrambleKey {string} the scramble key for the Sia app * @returns {Sia} a Sia instance with the Sia app open */ static async connectWebHID(scrambleKey = 'Sia') { return Sia.open(() => hw_transport_webhid_1.default.create(), scrambleKey); } /** * connectBLE connects to a Ledger over Web Bluetooth, launching the Sia app * from the dashboard if it isn't already open, and returns a ready Sia * instance. * @param scrambleKey {string} the scramble key for the Sia app * @returns {Sia} a Sia instance with the Sia app open */ static async connectBLE(scrambleKey = 'Sia') { return Sia.open(() => hw_transport_web_ble_1.default.create(), scrambleKey); } /** * supportedTransports returns the transport methods available in the current * environment. Web Bluetooth is excluded on Brave, which does not reliably * support it. * @returns {Array<'hid' | 'ble'>} the supported transport methods */ static async supportedTransports() { const nav = navigator; const support = await Promise.all([ hw_transport_webhid_1.default.isSupported().then(supported => supported ? 'hid' : null), hw_transport_web_ble_1.default.isSupported().then(async (supported) => supported && !(nav.brave && await nav.brave.isBrave()) ? 'ble' : null) ]); return support.filter((t) => t !== null); } /** * exchangeTxnHash sends an encoded transaction to the device in 255-byte * chunks and returns the response with the trailing status word stripped. */ async exchangeTxnHash(ins, encodedTxn, p2, sigIndex, keyIndex, changeIndex) { if (encodedTxn.length === 0) throw new Error('empty transaction'); const buf = buffer_1.Buffer.alloc(encodedTxn.length + 10); buf.writeUInt32LE(keyIndex, 0); buf.writeUInt16LE(sigIndex, 4); buf.writeUInt32LE(changeIndex, 6); buf.set(encodedTxn, 10); let resp = buffer_1.Buffer.alloc(0); for (let i = 0; i < buf.length; i += 255) { resp = await this.transport.send(CLA, ins, i === 0 ? P1_FIRST : P1_MORE, p2, buffer_1.Buffer.from(buf.subarray(i, i + 255))); } // the status code is appended as the last 2 bytes of the response, but // the transport already handles invalid codes. return buffer_1.Buffer.from(resp.subarray(0, resp.length - 2)); } /** * openApp launches the Sia app from the device's dashboard. The device * disconnects and reconnects when the app opens, so the transport must be * re-created before issuing further commands. */ async openApp() { await this.transport.send(CLA, INS_OPEN_APP, 0x00, 0x00, buffer_1.Buffer.from(APP_NAME, 'ascii')); } /** * getVersion returns the version of the Sia app * * @returns {string} the current version of the Sia app. */ async getVersion() { const resp = await this.transport.send(CLA, INS_GET_VERSION, 0x00, 0x00, buffer_1.Buffer.alloc(0)); return `v${resp[0]}.${resp[1]}.${resp[2]}`; } /** * getPublicKey returns the public key and standard Sia address for * the provided public key index. The user will be asked to verify the * public key on the display. A standard address is defined as an address * having 1 public key, requiring 1 signature, and no timelock. * @param index {number} the index of the public key * @returns {VerifyResponse} the public key and standard address */ async getPublicKey(index) { const resp = await this.transport.send(CLA, INS_GET_PUBLIC_KEY, 0x00, P2_DISPLAY_PUBKEY, uint32ToBuffer(index)); // the status code is appended as the last 2 bytes of the response, but // the transport already handles invalid codes. return { publicKey: `ed25519:${bytesToHex(resp.subarray(0, 32))}`, address: resp.subarray(32, resp.length - 2).toString() }; } /** * getAddress returns the public key and standard Sia address for * the provided public key index. The user will be asked to verify the * address on the display. A standard address is defined as an address * having 1 public key, requiring 1 signature, and no timelock. * @param index {number} the index of the public key * @returns {VerifyResponse} the public key and standard address */ async getAddress(index) { const resp = await this.transport.send(CLA, INS_GET_PUBLIC_KEY, 0x00, P2_DISPLAY_ADDRESS, uint32ToBuffer(index)); // the status code is appended as the last 2 bytes of the response, but // the transport already handles invalid codes. return { publicKey: `ed25519:${bytesToHex(resp.subarray(0, 32))}`, address: resp.subarray(32, resp.length - 2).toString() }; } /** * signV2Transaction signs the v2 transaction with the provided key * @param encodedTxn {Buffer} a sia encoded (V2TransactionSemantics) v2 transaction * @param sigIndex {number} the index of the signature to sign * @param keyIndex {number} the index of the key to sign with * @param changeIndex {number} the index of the key used for the change output * @returns {string} the hex encoded signature */ async signV2Transaction(encodedTxn, sigIndex, keyIndex, changeIndex) { const resp = await this.exchangeTxnHash(INS_CALC_V2TXN_HASH, encodedTxn, P2_SIGN_HASH, sigIndex, keyIndex, changeIndex); return bytesToHex(resp); } /** * signHash signs a 32-byte hash with the private key at the provided index * @param sigHash {Buffer} the 32-byte hash to sign * @param keyIndex {number} the index of the key to sign with * @returns {string} the hex encoded signature */ async signHash(sigHash, keyIndex) { const buf = buffer_1.Buffer.alloc(sigHash.length + 4); buf.writeUInt32LE(keyIndex, 0); buf.set(sigHash, 4); const resp = await this.transport.send(CLA, INS_SIGN_HASH, 0x00, 0x00, buf); return bytesToHex(resp.subarray(0, resp.length - 2)); } close() { return this.transport.close(); } } exports.default = Sia; //# sourceMappingURL=sia.js.map