UNPKG

@avalabs/hw-app-avalanche

Version:

Node API for Avalanche App (Ledger Nano S/X/S+)

473 lines (470 loc) 19.7 kB
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 };