@siacentral/ledgerjs-sia
Version:
Ledger hardware wallet Siacoin API.
230 lines • 9.76 kB
JavaScript
;
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