UNPKG

@mina-wallet-adapter/mina-ledger-js

Version:

JS API for Mina App (Ledger Nano S/X)

327 lines (323 loc) 9.37 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { MinaLedgerJS: () => MinaLedgerJS, Networks: () => Networks, TxType: () => TxType }); module.exports = __toCommonJS(src_exports); // src/types.ts var Networks = /* @__PURE__ */ ((Networks2) => { Networks2[Networks2["MAINNET"] = 1] = "MAINNET"; Networks2[Networks2["DEVNET"] = 0] = "DEVNET"; return Networks2; })(Networks || {}); var TxType = /* @__PURE__ */ ((TxType2) => { TxType2[TxType2["PAYMENT"] = 0] = "PAYMENT"; TxType2[TxType2["DELEGATION"] = 4] = "DELEGATION"; return TxType2; })(TxType || {}); // src/lib.ts var import_buffer = require("buffer"); var CLA_LEDGER = 176; var INS_LEDGER = { GET_NAME_VERSION: 1 }; var CLA_APP = 224; var INS_APP = { GET_VERSION: 1, GET_ADDR: 2, SIGN: 3 }; var SW_OK = 36864; var SW_CANCEL = 27014; var MinaLedgerJS = class { constructor(transport, scrambleKey = "carbonara") { if (transport === null || typeof transport === "undefined") { throw new Error("Transport cannot be empty"); } transport.setScrambleKey(scrambleKey); this.transport = transport; } pad(n, width = 3, paddingValue = 0) { return (String(paddingValue).repeat(width) + String(n)).slice( String(n).length ); } asciiToHex(str) { return import_buffer.Buffer.from(str, "ascii").toString("hex"); } convertMemo(memo) { const length = 32; let charToAdd = length - memo.length; let stringToReturn = memo; while (charToAdd > 0) { stringToReturn += "\0"; charToAdd--; } return import_buffer.Buffer.from(stringToReturn, "utf8").toString("hex"); } createTXApdu({ txType, senderAccount, senderAddress, receiverAddress, amount, fee, nonce, validUntil = 4294967295, memo = "", networkId }) { const senderBip44AccountHex = this.pad(senderAccount.toString(16), 8); const senderAddressHex = this.asciiToHex(senderAddress); const receiverHex = this.asciiToHex(receiverAddress); const amountHex = this.pad(amount.toString(16), 16); const feeHex = this.pad(fee.toString(16), 16); const nonceHex = this.pad(Number(nonce).toString(16).toUpperCase(), 8); const validUntilHex = this.pad(validUntil.toString(16), 8); const memoHex = this.convertMemo(memo); const tagHex = this.pad(txType.toString(16), 2); const networkIdHex = this.pad(networkId, 2); const apduMessage = senderBip44AccountHex + senderAddressHex + receiverHex + amountHex + feeHex + nonceHex + validUntilHex + memoHex + tagHex + networkIdHex; return apduMessage; } /** * Get Mina address for a given account number. * * @param account int of the account number * @param display optionally enable or not the display * @return an object with a publicKey * @example * const result = await Mina.getAddress(1); * const { publicKey, returnCode } = result; */ async getAddress(account = 0) { if (!Number.isInteger(account)) { return { publicKey: null, returnCode: "-5", message: "Account number must be Int", statusText: "ACCOUNT_NOT_INT" }; } const accountHex = this.pad(account.toString(16), 8, 0); const accountBuffer = import_buffer.Buffer.from(accountHex, "hex"); const p1 = 0; const p2 = 0; const statusList = [SW_OK, SW_CANCEL]; try { const response = await this.transport.send( CLA_APP, INS_APP.GET_ADDR, p1, p2, accountBuffer, statusList ); const publicKey = response.slice(0, response.length - 3).toString(); const returnCode = response.slice(response.length - 2, response.length).toString("hex"); if (returnCode !== SW_OK.toString(16)) { throw { returnCode, message: "unknown error", statusText: "UNKNOWN_ERROR" }; } return { publicKey, returnCode }; } catch (e) { return { publicKey: null, returnCode: e.returnCode?.toString() || "5000", message: e.message, statusText: e.statusText }; } } // /** // * Sign a Mina transaction with a given transaction // * // * @param Transaction arguments, see SignTransactionArgs type // * @return an object with signature and returnCode // */ async signTransaction({ txType, senderAccount, senderAddress, receiverAddress, amount, fee, nonce, validUntil = 4294967295, memo = "", networkId }) { if (isNaN(txType) || isNaN(senderAccount) || !senderAddress || !receiverAddress || !amount && txType === 0 /* PAYMENT */ || !fee || !Number.isInteger(amount) || !Number.isInteger(fee) || isNaN(nonce) || isNaN(networkId)) { return { signature: null, returnCode: "-1", message: "Missing or wrong arguments", statusText: "MISSING_ARGUMENTS" }; } if (memo.length > 32) { return { signature: null, returnCode: "-3", message: "Memo field too long", statusText: "MEMO_TOO_BIG" }; } if (fee < 1e6) { return { signature: null, returnCode: "-4", message: "Fee too small", statusText: "FEE_TOO_SMALL" }; } const apdu = this.createTXApdu({ txType, senderAccount, senderAddress, receiverAddress, amount, fee, nonce, validUntil, memo, networkId }); const apduBuffer = import_buffer.Buffer.from(apdu, "hex"); const p1 = 0; const p2 = 0; const statusList = [SW_OK, SW_CANCEL]; if (apduBuffer.length > 256) { return { signature: null, returnCode: "-2", message: "data length > 256 bytes", statusText: "DATA_TOO_BIG" }; } try { const response = await this.transport.send( CLA_APP, INS_APP.SIGN, p1, p2, apduBuffer, statusList ); const signature = response.slice(0, response.length - 2).toString("hex"); const returnCode = response.slice(response.length - 2, response.length).toString("hex"); return { signature, returnCode }; } catch (e) { return { signature: null, returnCode: e.statusCode.toString(), message: e.message, statusText: e.statusText }; } } /** * get the version of the Mina app installed on the hardware device * the version is returned from the installed app. * * @return an object with a version */ async getAppVersion() { try { const p1 = 0; const p2 = 0; const response = await this.transport.send( CLA_APP, INS_APP.GET_VERSION, p1, p2 ); const versionRaw = response.slice(0, response.length - 2).toString("hex"); const version = "" + versionRaw[1] + "." + versionRaw[2] + "." + versionRaw[3]; const returnCode = response.slice(response.length - 2, response.length).toString("hex"); return { version, returnCode }; } catch (e) { return { version: null, returnCode: e.statusCode?.toString() || "5000", message: e.message, statusText: e.statusText }; } } /** * get the name and version of the Mina app installed on the hardware device * it can be used to ping the app and know the name of the running app. * The name and version is returned from the Ledger firmware. * * @return an object with an app name and version */ async getAppName() { try { const p1 = 0; const p2 = 0; const response = await this.transport.send( CLA_LEDGER, INS_LEDGER.GET_NAME_VERSION, p1, p2 ); const returnCode = response.slice(response.length - 2, response.length).toString("hex"); const separatorPosition = response.indexOf(5); const name = response.slice(2, separatorPosition).toString("ascii"); const version = response.slice(separatorPosition + 1, response.length - 4).toString("utf-8"); return { name, // Mina version, // 1.0.0 returnCode }; } catch (e) { return { version: null, returnCode: e.statusCode?.toString() || "5000", message: e.message, statusText: e.statusText }; } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MinaLedgerJS, Networks, TxType });