@avalabs/hw-app-avalanche
Version:
Node API for Avalanche App (Ledger Nano S/X/S+)
473 lines (470 loc) • 19.7 kB
JavaScript
import { CHUNK_SIZE, CLA, LedgerError, errorCodeToString, processErrorResponse, INS, HASH_LEN, FIRST_MESSAGE, P2_VALUES, LAST_MESSAGE, NEXT_MESSAGE, getVersion, COLLECTION_NAME_MAX_LEN, ADDRESS_LENGTH, CHAIN_ID_SIZE, ALGORITHM_ID_SIZE, SIGNATURE_LENGTH_SIZE, TYPE_1, TYPE_SIZE, VERSION_1, VERSION_SIZE, ALGORITHM_ID_1, ED25519_PK_SIZE, PAYLOAD_TYPE, P1_VALUES } from './common.js';
import { serializePath, serializePathSuffix, pathCoinType, serializeHrp, serializeChainID } from './helper.js';
import Eth from '@ledgerhq/hw-app-eth';
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
function processGetAddrResponse(response) {
let partialResponse = response;
const errorCodeData = partialResponse.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const PKLEN = partialResponse[0];
const publicKey = Buffer.from(partialResponse.slice(1, 1 + PKLEN));
partialResponse = partialResponse.slice(1 + PKLEN);
let hash;
let address;
if (PKLEN != ED25519_PK_SIZE) {
hash = Buffer.from(partialResponse.slice(0, 20));
partialResponse = partialResponse.slice(20);
address = Buffer.from(partialResponse.subarray(0, -2)).toString();
} else {
address = partialResponse.subarray(0, -2).toString("hex");
}
return {
publicKey,
hash,
address,
returnCode,
errorMessage: errorCodeToString(returnCode)
};
}
function processGetXPubResponse(response) {
let partialResponse = response;
const errorCodeData = partialResponse.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const PKLEN = partialResponse[0];
const publicKey = Buffer.from(partialResponse.slice(1, 1 + PKLEN));
partialResponse = partialResponse.slice(1 + PKLEN);
const chain_code = Buffer.from(partialResponse.slice(0, -2));
return {
publicKey,
chain_code,
returnCode,
errorMessage: errorCodeToString(returnCode)
};
}
class AvalancheApp {
constructor(transport, ethScrambleKey = "w0w", ethLoadConfig = {}) {
__publicField(this, "transport");
__publicField(this, "eth");
this.transport = transport;
if (!transport) {
throw new Error("Transport has not been defined");
}
this.eth = new Eth(transport, ethScrambleKey, ethLoadConfig);
}
static prepareChunks(message, serializedPathBuffer) {
const chunks = [];
if (serializedPathBuffer !== void 0) {
chunks.push(serializedPathBuffer);
}
const buffer = Buffer.from(message);
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
let end = i + CHUNK_SIZE;
if (i > buffer.length) {
end = buffer.length;
}
chunks.push(buffer.slice(i, end));
}
return chunks;
}
async signGetChunks(message, path) {
if (path === void 0) {
return AvalancheApp.prepareChunks(message, Buffer.alloc(0));
} else {
return AvalancheApp.prepareChunks(message, serializePath(path));
}
}
concatMessageAndChangePath(message, path) {
const msg = message;
if (path === void 0) {
const buffer = Buffer.alloc(1);
buffer.writeUInt8(0);
return Buffer.concat([new Uint8Array(buffer), new Uint8Array(msg)]);
} else {
let buffer = Buffer.alloc(1);
buffer.writeUInt8(path.length);
path.forEach((element) => {
buffer = Buffer.concat([new Uint8Array(buffer), new Uint8Array(serializePathSuffix(element))]);
});
return Buffer.concat([new Uint8Array(buffer), new Uint8Array(msg)]);
}
}
async signSendChunk(chunkIdx, chunkNum, chunk, param, ins = INS.SIGN) {
let payloadType = PAYLOAD_TYPE.ADD;
let p2 = 0;
if (chunkIdx === 1) {
payloadType = PAYLOAD_TYPE.INIT;
if (param === void 0) {
throw Error("number type not given");
}
p2 = param;
}
if (chunkIdx === chunkNum) {
payloadType = PAYLOAD_TYPE.LAST;
}
return this.transport.send(CLA, ins, payloadType, p2, chunk, [
LedgerError.NoErrors,
LedgerError.DataIsInvalid,
LedgerError.BadKeyHandle,
LedgerError.SignVerifyError
]).then((response) => {
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
let errorMessage = errorCodeToString(returnCode);
if (returnCode === LedgerError.BadKeyHandle || returnCode === LedgerError.DataIsInvalid || returnCode === LedgerError.SignVerifyError) {
errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`;
}
if (returnCode === LedgerError.NoErrors && response.length > 2) {
return {
hash: null,
signature: null,
returnCode,
errorMessage
};
}
return {
returnCode,
errorMessage
};
}, processErrorResponse);
}
async signHash(path_prefix, signing_paths, hash, curve_type = P2_VALUES.SECP256K1) {
if (hash.length !== HASH_LEN) {
throw new Error("Invalid hash length");
}
const first_response = await this.transport.send(CLA, INS.SIGN_HASH, FIRST_MESSAGE, 0, Buffer.concat([new Uint8Array(serializePath(path_prefix)), new Uint8Array(hash)]), [LedgerError.NoErrors]).then((response) => {
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
let errorMessage = errorCodeToString(returnCode);
if (returnCode === LedgerError.BadKeyHandle || returnCode === LedgerError.DataIsInvalid) {
errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`;
}
return {
returnCode,
errorMessage
};
}, processErrorResponse);
if (first_response.returnCode !== LedgerError.NoErrors) {
return first_response;
}
return this._signAndCollect(signing_paths, curve_type);
}
async _signAndCollect(signing_paths, curve_type) {
const result = {
returnCode: LedgerError.NoErrors,
errorMessage: "",
hash: null,
signatures: null
};
const signatures = /* @__PURE__ */ new Map();
for (let idx = 0; idx < signing_paths.length; idx++) {
const suffix = signing_paths[idx];
const path_buf = serializePathSuffix(suffix);
const p1 = idx >= signing_paths.length - 1 ? LAST_MESSAGE : NEXT_MESSAGE;
await this.transport.send(CLA, INS.SIGN_HASH, p1, curve_type, path_buf, [
LedgerError.NoErrors,
LedgerError.DataIsInvalid,
LedgerError.BadKeyHandle,
LedgerError.SignVerifyError
]).then((response) => {
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const errorMessage = errorCodeToString(returnCode);
if (returnCode === LedgerError.BadKeyHandle || returnCode === LedgerError.DataIsInvalid || returnCode === LedgerError.SignVerifyError) {
result.errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`;
}
if (returnCode === LedgerError.NoErrors && response.length > 2) {
signatures.set(suffix, response.slice(0, -2));
}
result.returnCode = returnCode;
result.errorMessage = errorMessage;
return;
}, processErrorResponse);
if (result.returnCode !== LedgerError.NoErrors) {
break;
}
}
result.signatures = signatures;
return result;
}
async sign(path_prefix, signing_paths, message, change_paths, curve_type = P2_VALUES.SECP256K1) {
let paths = signing_paths;
if (change_paths !== void 0) {
paths = [.../* @__PURE__ */ new Set([...paths, ...change_paths])];
}
const msg = this.concatMessageAndChangePath(message, paths);
const response = await this.signGetChunks(msg, path_prefix).then((chunks) => {
return this.signSendChunk(1, chunks.length, chunks[0], FIRST_MESSAGE, INS.SIGN).then(async (response2) => {
let result = {
returnCode: response2.returnCode,
errorMessage: response2.errorMessage,
signatures: null
};
for (let i = 1; i < chunks.length; i += 1) {
result = await this.signSendChunk(1 + i, chunks.length, chunks[i], NEXT_MESSAGE, INS.SIGN);
if (result.returnCode !== LedgerError.NoErrors) {
break;
}
}
return result;
}, processErrorResponse);
}, processErrorResponse);
if (response.returnCode !== LedgerError.NoErrors) {
return response;
}
return this._signAndCollect(signing_paths, curve_type);
}
// Sign an arbitrary message.
// This function takes in an avax path prefix like: m/44'/9000'/0'/0'
// signing_paths: ["0/1", "5/8"]
// message: The message to be signed
async signMsg(path_prefix, signing_paths, message, curve_type = P2_VALUES.SECP256K1) {
const coinType = pathCoinType(path_prefix);
if (coinType !== "9000'") {
throw new Error("Only avax path is supported");
}
const header = Buffer.from("Avalanche Signed Message:\n", "utf8");
const content = Buffer.from(message, "utf8");
const msgSize = Buffer.alloc(4);
msgSize.writeUInt32BE(content.length, 0);
const avax_msg = Buffer.from(`${header}${msgSize}${content}`, "utf8");
const response = await this.signGetChunks(avax_msg, path_prefix).then((chunks) => {
return this.signSendChunk(1, chunks.length, chunks[0], FIRST_MESSAGE, INS.SIGN_MSG).then(async (response2) => {
let result = {
returnCode: response2.returnCode,
errorMessage: response2.errorMessage,
signatures: null
};
for (let i = 1; i < chunks.length; i += 1) {
result = await this.signSendChunk(1 + i, chunks.length, chunks[i], NEXT_MESSAGE, INS.SIGN_MSG);
if (result.returnCode !== LedgerError.NoErrors) {
break;
}
}
return result;
}, processErrorResponse);
}, processErrorResponse);
if (response.returnCode !== LedgerError.NoErrors) {
return response;
}
return this._signAndCollect(signing_paths, curve_type);
}
async getVersion() {
return getVersion(this.transport).catch((err) => processErrorResponse(err));
}
async getAppInfo() {
return this.transport.send(176, 1, 0, 0).then((response) => {
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
let appName = "err";
let appVersion = "err";
let flagLen = 0;
let flagsValue = 0;
if (response[0] !== 1) {
LedgerError.DeviceIsBusy;
} else {
const appNameLen = response[1];
appName = response.slice(2, 2 + appNameLen).toString("ascii");
let idx = 2 + appNameLen;
const appVersionLen = response[idx];
idx += 1;
appVersion = response.slice(idx, idx + appVersionLen).toString("ascii");
idx += appVersionLen;
const appFlagsLen = response[idx];
idx += 1;
flagLen = appFlagsLen;
flagsValue = response[idx];
}
return {
returnCode,
errorMessage: errorCodeToString(returnCode),
//
appName,
appVersion,
flagLen,
flagsValue,
flagRecovery: (flagsValue & 1) !== 0,
// eslint-disable-next-line no-bitwise
flagSignedMcuCode: (flagsValue & 2) !== 0,
// eslint-disable-next-line no-bitwise
flagOnboarded: (flagsValue & 4) !== 0,
// eslint-disable-next-line no-bitwise
flagPINValidated: (flagsValue & 128) !== 0
};
}, processErrorResponse);
}
async _pubkey(path, show, hrp, chainid, curve_type = P2_VALUES.SECP256K1) {
const p1 = show ? P1_VALUES.SHOW_ADDRESS_IN_DEVICE : P1_VALUES.ONLY_RETRIEVE;
const serializedPath = serializePath(path);
if (curve_type !== P2_VALUES.SECP256K1 && curve_type !== P2_VALUES.ED25519) {
throw new Error("Invalid curve type. Must be 0 for secp256k1 or 1 for ed25519");
}
const payload = curve_type === P2_VALUES.SECP256K1 ? Buffer.concat([
new Uint8Array(serializeHrp(hrp)),
new Uint8Array(serializeChainID(chainid)),
new Uint8Array(serializedPath)
]) : serializedPath;
return this.transport.send(CLA, INS.GET_ADDR, p1, curve_type, payload, [LedgerError.NoErrors]).then(processGetAddrResponse, processErrorResponse);
}
async getAddressAndPubKey(path, show, hrp, chainid, curve_type = P2_VALUES.SECP256K1) {
return this._pubkey(path, show, hrp, chainid, curve_type);
}
async _xpub(path, show, hrp, chainid) {
const p1 = show ? P1_VALUES.SHOW_ADDRESS_IN_DEVICE : P1_VALUES.ONLY_RETRIEVE;
const serializedPath = serializePath(path);
const serializedHrp = serializeHrp(hrp);
const serializedChainID = serializeChainID(chainid);
return this.transport.send(CLA, INS.GET_EXTENDED_PUBLIC_KEY, p1, 0, Buffer.concat([new Uint8Array(serializedHrp), new Uint8Array(serializedChainID), new Uint8Array(serializedPath)]), [
LedgerError.NoErrors
]).then(processGetXPubResponse, processErrorResponse);
}
async getExtendedPubKey(path, show, hrp, chainid) {
return this._xpub(path, show, hrp, chainid);
}
async _walletId(show) {
const p1 = show ? P1_VALUES.SHOW_ADDRESS_IN_DEVICE : P1_VALUES.ONLY_RETRIEVE;
return this.transport.send(CLA, INS.WALLET_ID, p1, 0).then((response) => {
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
return {
returnCode,
errorMessage: errorCodeToString(returnCode),
id: response.slice(0, 6)
};
}, processErrorResponse);
}
async getWalletId() {
return this._walletId(false);
}
async showWalletId() {
return this._walletId(true);
}
signEVMTransaction(path, rawTxHex, resolution) {
return this.eth.signTransaction(path, rawTxHex, resolution);
}
getETHAddress(path, boolDisplay, boolChaincode) {
return this.eth.getAddress(path, boolDisplay, boolChaincode);
}
getAppConfiguration() {
return this.eth.getAppConfiguration();
}
async provideERC20TokenInformation(ticker, contractName, address, decimals, chainId) {
const tickerLength = Buffer.byteLength(ticker);
const contractNameLength = Buffer.byteLength(contractName);
const buffer = Buffer.alloc(1 + tickerLength + 1 + contractNameLength + 20 + 4 + 4);
let offset = 0;
buffer.writeUInt8(tickerLength, offset);
offset += 1;
buffer.write(ticker, offset);
offset += tickerLength;
buffer.writeUInt8(contractNameLength, offset);
offset += 1;
buffer.write(contractName, offset);
offset += contractNameLength;
var addr_offset = 0;
if (address.startsWith("0x")) {
addr_offset = 2;
}
const addressBuffer = Buffer.from(address.slice(addr_offset), "hex");
addressBuffer.copy(new Uint8Array(buffer), offset);
offset += 20;
buffer.writeUInt32BE(decimals, offset);
offset += 4;
buffer.writeUInt32BE(chainId, offset);
offset += 4;
return this.eth.provideERC20TokenInformation(buffer.toString("hex"));
}
async provideNFTInformation(collectionName, contractAddress, chainId) {
const NAME_LENGTH_SIZE = 1;
const HEADER_SIZE = TYPE_SIZE + VERSION_SIZE + NAME_LENGTH_SIZE;
const KEY_ID_SIZE = 1;
const PROD_NFT_METADATA_KEY = 1;
const collectionNameLength = Buffer.byteLength(collectionName, "utf8");
if (collectionNameLength > COLLECTION_NAME_MAX_LEN) {
throw new Error(`Collection name exceeds maximum allowed length of ${COLLECTION_NAME_MAX_LEN}`);
}
const fakeDerSignature = this._generateFakeDerSignature();
const buffer = Buffer.alloc(
HEADER_SIZE + collectionNameLength + ADDRESS_LENGTH + CHAIN_ID_SIZE + KEY_ID_SIZE + ALGORITHM_ID_SIZE + SIGNATURE_LENGTH_SIZE + fakeDerSignature.length
);
let offset = 0;
buffer.writeUInt8(TYPE_1, offset);
offset += TYPE_SIZE;
buffer.writeUInt8(VERSION_1, offset);
offset += VERSION_SIZE;
buffer.writeUInt8(collectionNameLength, offset);
offset += NAME_LENGTH_SIZE;
buffer.write(collectionName, offset, "utf8");
offset += collectionNameLength;
Buffer.from(contractAddress.slice(2), "hex").copy(new Uint8Array(buffer), offset);
offset += ADDRESS_LENGTH;
buffer.writeBigUInt64BE(chainId, offset);
offset += CHAIN_ID_SIZE;
buffer.writeUInt8(PROD_NFT_METADATA_KEY, offset);
offset += KEY_ID_SIZE;
buffer.writeUInt8(ALGORITHM_ID_1, offset);
offset += ALGORITHM_ID_SIZE;
buffer.writeUInt8(fakeDerSignature.length, offset);
offset += SIGNATURE_LENGTH_SIZE;
fakeDerSignature.copy(new Uint8Array(buffer), offset);
return this.eth.provideNFTInformation(buffer.toString("hex"));
}
_generateFakeDerSignature() {
const fakeSignatureLength = 70;
const fakeDerSignature = Buffer.alloc(fakeSignatureLength);
for (let i = 0; i < fakeSignatureLength; i++) {
fakeDerSignature[i] = Math.floor(Math.random() * 256);
}
return fakeDerSignature;
}
// We assume pluginName is ERC721 for Nft tokens
async setPlugin(contractAddress, methodSelector, chainId) {
const KEY_ID = 2;
const PLUGIN_NAME_LENGTH_SIZE = 1;
const KEY_ID_SIZE = 1;
const PLUGIN_NAME = "ERC721";
const pluginNameBuffer = Buffer.from(PLUGIN_NAME, "utf8");
const pluginNameLength = pluginNameBuffer.length;
const contractAddressBuffer = Buffer.from(contractAddress.slice(2), "hex");
const methodSelectorBuffer = Buffer.from(methodSelector.slice(2), "hex");
const signatureBuffer = this._generateFakeDerSignature();
const signatureLength = signatureBuffer.length;
const buffer = Buffer.alloc(
TYPE_SIZE + VERSION_SIZE + PLUGIN_NAME_LENGTH_SIZE + pluginNameLength + contractAddressBuffer.length + methodSelectorBuffer.length + CHAIN_ID_SIZE + KEY_ID_SIZE + ALGORITHM_ID_SIZE + SIGNATURE_LENGTH_SIZE + signatureLength
);
let offset = 0;
buffer.writeUInt8(TYPE_1, offset);
offset += TYPE_SIZE;
buffer.writeUInt8(VERSION_1, offset);
offset += VERSION_SIZE;
buffer.writeUInt8(pluginNameLength, offset);
offset += PLUGIN_NAME_LENGTH_SIZE;
pluginNameBuffer.copy(new Uint8Array(buffer), offset);
offset += pluginNameLength;
contractAddressBuffer.copy(new Uint8Array(buffer), offset);
offset += contractAddressBuffer.length;
methodSelectorBuffer.copy(new Uint8Array(buffer), offset);
offset += methodSelectorBuffer.length;
buffer.writeBigUInt64BE(BigInt(chainId), offset);
offset += CHAIN_ID_SIZE;
buffer.writeUInt8(KEY_ID, offset);
offset += KEY_ID_SIZE;
buffer.writeUInt8(ALGORITHM_ID_1, offset);
offset += ALGORITHM_ID_SIZE;
buffer.writeUInt8(signatureLength, offset);
offset += SIGNATURE_LENGTH_SIZE;
signatureBuffer.copy(new Uint8Array(buffer), offset);
return this.eth.setPlugin(buffer.toString("hex"));
}
async clearSignTransaction(path, rawTxHex, resolutionConfig, throwOnError = false) {
return this.eth.clearSignTransaction(path, rawTxHex, resolutionConfig, throwOnError);
}
async signEIP712Message(path, jsonMessage, fullImplem = false) {
return this.eth.signEIP712Message(path, jsonMessage, fullImplem);
}
async signEIP712HashedMessage(path, domainSeparatorHex, hashStructMessageHex) {
return this.eth.signEIP712HashedMessage(path, domainSeparatorHex, hashStructMessageHex);
}
}
export { LedgerError, AvalancheApp as default };