@mina-wallet-adapter/mina-ledger-js
Version:
JS API for Mina App (Ledger Nano S/X)
327 lines (323 loc) • 9.37 kB
JavaScript
;
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
});