UNPKG

iso-filecoin

Version:

Isomorphic filecoin abstractions for RPC, signatures, address, token and wallet

429 lines (378 loc) 11 kB
'use strict'; var index = require('../node_modules/.pnpm/@ledgerhq_errors@6.23.0/node_modules/@ledgerhq/errors/lib-es/index.cjs'); var secp256k1 = require('../node_modules/.pnpm/@noble_curves@1.9.4/node_modules/@noble/curves/esm/secp256k1.cjs'); var blake2b = require('../node_modules/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/blake2b.cjs'); var rfc4648 = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/rfc4648.cjs'); var utf8 = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/utf8.cjs'); var utils = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/utils.cjs'); var varint = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/varint.cjs'); var src_address = require('./address.cjs'); var src_utils = require('./utils.cjs'); /** * @typedef {import('./types.js').Transport} Transport * @typedef {keyof typeof SIGNATURE_TYPE} SignatureType */ /** * Filecoin App APDU return messages * * @see https://github.com/Zondax/ledger-filecoin/blob/main/docs/APDUSPEC.md#return-codes * @type {Record<string, string>} * */ const RETURN_MSGS = { 0x9000: 'Success', 0x9001: 'Busy', 0x6400: 'Execution Error', 0x6700: 'Wrong Length', 0x6982: 'Empty buffer', 0x6983: 'Output buffer too small', 0x6984: 'Data is invalid', 0x6985: 'Conditions not satisfied', 0x6986: 'Command rejected', 0x6a80: 'Bad key handle', 0x6b00: 'Invalid parameter(s)', 0x6d00: 'Instriction not supported', 0x6e00: 'Application not supported', 0x6e01: 'Filecoin app not open', 0x6f00: 'Unknown', 0x6f01: 'Sign/verify error', }; /** * APDU codes * * @see https://github.com/tendermint/ledger-validator-app/blob/master/deps/ledger-zxlib/include/apdu_codes.h */ const APDU_CODES = { // ledger supports OK: 0x9000, BUSY: 0x9001, EXECUTION_ERROR: 0x6400, // ledger supports WRONG_LENGTH: 0x6700, /** * Ledger name is SECURITY_STATUS_NOT_SATISFIED * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L286 */ EMPTY_BUFFER: 0x6982, OUTPUT_BUFFER_TOO_SMALL: 0x6983, DATA_INVALID: 0x6984, /** * ledger supports * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L258 */ CONDITIONS_NOT_SATISFIED: 0x6985, COMMAND_NOT_ALLOWED: 0x6986, /** * Ledger name is INCORRECT_DATA * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L268 */ BAD_KEY_HANDLE: 0x6a80, /** * ledger supports * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L270 */ INVALIDP1P2: 0x6b00, // ledger supports INS_NOT_SUPPORTED: 0x6d00, // ledger supports CLA_NOT_SUPPORTED: 0x6e00, // ledger supports UNKNOWN: 0x6f00, SIGN_VERIFY_ERROR: 0x6f01, APP_NOT_OPEN: 0x6e01, }; const EIP191_PREFIX = 'Filecoin Sign Bytes:\n'; const IS_HID_SUPPORTED = // @ts-ignore 'navigator' in globalThis && navigator.hid !== undefined; const CHUNK_SIZE = 250; /** * @see https://github.com/Zondax/ledger-filecoin/blob/main/docs/APDUSPEC.md */ const CLA = 0x06; const INS = { GET_VERSION: 0x00, GET_ADDR_SECP256K1: 0x01}; const SIGNATURE_TYPE = { SECP256K1: 0x02, DATA_CAP: 0x05, CLIENT_DEAL: 0x06, RAW_BYTES: 0x07, PERSONAL_MESSAGE: 0x08, }; /** * Serialize derivation path * * @param {string} path */ function serializeDerivationPath(path) { const pathComponents = src_utils.parseDerivationPath(path); const bytes = new ArrayBuffer(20); const view = new DataView(bytes); view.setUint32(0, 0x80000000 + pathComponents.purpose, true); view.setUint32(4, 0x80000000 + pathComponents.coinType, true); view.setUint32(8, 0x80000000 + pathComponents.account, true); view.setUint32(12, pathComponents.change, true); view.setUint32(16, pathComponents.addressIndex, true); return utils.buf(view) } /** * Filecoin app error */ class FilecoinAppError extends Error { name = 'FilecoinAppError' /** @type {number} */ statusCode /** * @param {number} statusCode The error status code coming from a Transport implementation * @param {string} [data] The error message coming from a instruction call */ constructor(statusCode, data) { const statusCodeStr = statusCode.toString(16); const message = `Filecoin App: ${RETURN_MSGS[statusCode]}${data ? ` ${data}` : ''} (0x${statusCodeStr})`; super(message); this.statusCode = statusCode; } } /** * Check for error returned in the APDU response * * @param {Buffer} output */ function checkError(output) { const errorCodeData = output.subarray(-2); const code = rfc4648.hex.encode(errorCodeData); const intCode = Number.parseInt(code, 16); if ( intCode === APDU_CODES.DATA_INVALID || intCode === APDU_CODES.BAD_KEY_HANDLE ) { return new FilecoinAppError( intCode, output.subarray(0, output.length - 2).toString('ascii') ) } if (intCode !== APDU_CODES.OK) { return new FilecoinAppError(intCode) } } /** * Sign chunk of data * * @param {Transport} transport - Ledger transport * @param {number} index * @param {number} size * @param {Uint8Array} data * @param {number} [instruction=0x02] */ async function signChunk(transport, index$1, size, data, instruction = 0x02) { let payloadDesc = 0x00; if (index$1 !== 0) { payloadDesc = 0x01; } if (index$1 === size - 1) { payloadDesc = 0x02; } const out = await transport.send( 0x06, instruction, payloadDesc, 0, utils.buf(data), [ index.StatusCodes.OK, APDU_CODES.BAD_KEY_HANDLE, APDU_CODES.DATA_INVALID, APDU_CODES.APP_NOT_OPEN, APDU_CODES.COMMAND_NOT_ALLOWED, ] ); const err = checkError(out); if (err) { throw err } return out.subarray(0, 65) } /** * Verify raw signature * * @param {Uint8Array} signature * @param {Uint8Array} data * @param {Uint8Array} publicKey * @returns {boolean} * @example * ```ts twoslash * import { verifyRaw } from 'iso-filecoin/ledger' * * const signature = new Uint8Array([1, 2, 3]) * const data = new Uint8Array([4, 5, 6]) * const publicKey = new Uint8Array([7, 8, 9]) * const isValid = verifyRaw(signature, data, publicKey) * // => true * ``` */ function verifyRaw(signature, data, publicKey) { const prefix = utf8.utf8.decode(EIP191_PREFIX); const prefixed = utils.concat([prefix, data]); const cid = src_utils.lotusCid(prefixed); return secp256k1.secp256k1.verify( signature.subarray(0, 64), blake2b.blake2b(cid, { dkLen: 32, }), publicKey ) } /** * Ledger Filecoin app client */ class LedgerFilecoin { /** * * @param {Transport} transport - Ledger transport */ constructor(transport) { this.transport = transport; transport.decorateAppAPIMethods( this, ['getVersion', 'getAddress', 'sign'], 'Filecoin' ); } /** * Get the version of the Filecoin app * * @see https://github.com/LedgerHQ/app-filecoin/blob/develop/docs/APDUSPEC.md#get_version * @example * ```ts twoslash * import { LedgerFilecoin } from 'iso-filecoin/ledger' * import TransportWebUSB from '@ledgerhq/hw-transport-webusb' * * const transport = await TransportWebUSB.create() * const ledger = new LedgerFilecoin(transport) * const version = await ledger.getVersion() * // => '1.0.0' * ``` */ async getVersion() { const out = await this.transport.send( CLA, INS.GET_VERSION, 0, 0, undefined, [index.StatusCodes.OK, APDU_CODES.APP_NOT_OPEN] ); const err = checkError(out); if (err) { throw err } return out.subarray(1, 4).join('.') } /** * Get the secp256k1 address for a given derivation path * * @see https://github.com/LedgerHQ/app-filecoin/blob/develop/docs/APDUSPEC.md#ins_get_addr_secp256k1 * * @param {string} path - Derivation path * @param {boolean} [showOnDevice=false] - Whether to show the address on the device * @returns {Promise<import('./types.js').IAccount>} */ async getAddress(path, showOnDevice = false) { const out = await this.transport.send( CLA, INS.GET_ADDR_SECP256K1, showOnDevice ? 0x01 : 0x00, 0x00, serializeDerivationPath(path), [index.StatusCodes.OK, APDU_CODES.APP_NOT_OPEN, APDU_CODES.COMMAND_NOT_ALLOWED] ); const err = checkError(out); if (err) { throw err } const publicKey = utils.u8(out.subarray(0, 65)); return { type: 'SECP256K1', address: src_address.AddressSecp256k1.fromPublicKey( publicKey, src_utils.getNetworkFromPath(path) ), publicKey, path, } } /** * Sign a message * * @param {string} path - Derivation path * @param {Uint8Array} message - Message to sign in bytes * @param {SignatureType} [type=SECP256K1] - Signature type */ async sign(path, message, type = 'SECP256K1') { const chunks = []; chunks.push(serializeDerivationPath(path)); for ( let start = 0, end = 1; start < message.length; start += CHUNK_SIZE, end++ ) { chunks.push(message.subarray(start, CHUNK_SIZE * end)); } let result; for (let index = 0; index < chunks.length; index++) { result = await signChunk( this.transport, index, chunks.length, chunks[index], SIGNATURE_TYPE[type] ); } if (!result) { throw new Error('Sign failed') } return new Uint8Array(result.buffer, result.byteOffset, result.byteLength) } /** * Sign raw bytes using prefixed message similar to EIP-191 * * @param {string} path - Derivation path * @param {Uint8Array} message - Message to sign */ signRaw(path, message) { const prefix = utf8.utf8.decode(EIP191_PREFIX); const prefixed = utils.concat([prefix, message]); const data = utils.concat([varint.encode(prefixed.length)[0], prefixed]); return this.sign(path, data, 'RAW_BYTES') } /** * Sign a message using FRC-102 * * @param {string} path - Derivation path * @param {Uint8Array} message - Message to sign */ personalSign(path, message) { const buf = new Uint8Array(4); const view = new DataView(buf.buffer); view.setUint32(0, message.length, false); const data = utils.concat([buf, message]); return this.sign(path, data, 'PERSONAL_MESSAGE') } /** * Close the transport */ close() { return this.transport.close() } } exports.APDU_CODES = APDU_CODES; exports.EIP191_PREFIX = EIP191_PREFIX; exports.FilecoinAppError = FilecoinAppError; exports.IS_HID_SUPPORTED = IS_HID_SUPPORTED; exports.LedgerFilecoin = LedgerFilecoin; exports.verifyRaw = verifyRaw;