UNPKG

@magickbase/hw-app-ckb

Version:

Ledger Hardware Wallet Nervos CKB API

336 lines (276 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _bip32Path = require("bip32-path"); var _bip32Path2 = _interopRequireDefault(_bip32Path); var _annotated = require("./annotated"); var blockchain = _interopRequireWildcard(_annotated); var _blake2bWasm = require("blake2b-wasm"); var _blake2bWasm2 = _interopRequireDefault(_blake2bWasm); var _bech = require("bech32"); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } // CKB address is longer than the longest Bitcoin address // The bech32m encoding limit should be increased const BECH32_LIMIT = 1023; /** * Nervos API * * @example * import Ckb from "@magickbase/hw-app-ckb"; * const ckb = new Ckb(transport); */ class Ckb { constructor(transport, scrambleKey = "CKB") { this.defaultSighashWitness = "55000000100000005500000055000000410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; this.transport = transport; transport.decorateAppAPIMethods(this, ["getAppConfiguration", "getWalletId", "getWalletPublicKey", "signAnnotatedTransaction"], scrambleKey); } /** * get CKB address for a given BIP 32 path. * * @param path a path in BIP 32 format * @return an object with a publicKey, lockArg, and (secp256k1+blake160) address. * @example * const result = await ckb.getWalletPublicKey("44'/144'/0'/0/0"); * const publicKey = result.publicKey; * const lockArg = result.lockArg; * const address = result.address; */ getWalletPublicKey(path, testnet) { var _this = this; return _asyncToGenerator(function* () { const bipPath = _bip32Path2.default.fromString(path).toPathArray(); const cla = 0x80; const ins = 0x02; const p1 = 0x00; const p2 = 0x00; const data = Buffer.alloc(1 + bipPath.length * 4); data.writeUInt8(bipPath.length, 0); bipPath.forEach(function (segment, index) { data.writeUInt32BE(segment, 1 + index * 4); }); const response = yield _this.transport.send(cla, ins, p1, p2, data); const publicKeyLength = response[0]; const publicKey = response.slice(1, 1 + publicKeyLength); const compressedPublicKey = Buffer.alloc(33); compressedPublicKey.fill(publicKey[64] & 1 ? "03" : "02", 0, 1, "hex"); compressedPublicKey.fill(publicKey.subarray(1, 33), 1, 33); const hashPersonalization = Uint8Array.from([99, 107, 98, 45, 100, 101, 102, 97, 117, 108, 116, 45, 104, 97, 115, 104]); const lockArg = Buffer.from((0, _blake2bWasm2.default)(32, null, null, hashPersonalization).update(compressedPublicKey).digest("binary").subarray(0, 20)); const addr_contents = [ // CKB 2021 address full format prefix 0x00, // SECP256K1_BLAKE160 code hash ...[155, 215, 224, 111, 62, 207, 75, 224, 242, 252, 210, 24, 139, 35, 241, 185, 252, 200, 142, 93, 75, 101, 168, 99, 123, 23, 114, 59, 189, 163, 204, 232], // SECP256K1_BLAKE160 hash type 0b00000001, // lock args ...lockArg]; const addr = _bech.bech32m.encode(testnet ? "ckt" : "ckb", _bech.bech32m.toWords(addr_contents), BECH32_LIMIT); return { publicKey: publicKey.toString("hex"), lockArg: lockArg.toString("hex"), address: addr }; })(); } /** * get extended public key for a given BIP 32 path. * * @param path a path in BIP 32 format * @return an object with a publicKey * @example * const result = await ckb.getWalletPublicKey("44'/144'/0'/0/0"); * const publicKey = result; */ getWalletExtendedPublicKey(path) { var _this2 = this; return _asyncToGenerator(function* () { const bipPath = _bip32Path2.default.fromString(path).toPathArray(); const cla = 0x80; const ins = 0x04; const p1 = 0x00; const p2 = 0x00; const data = Buffer.alloc(1 + bipPath.length * 4); data.writeUInt8(bipPath.length, 0); bipPath.forEach(function (segment, index) { data.writeUInt32BE(segment, 1 + index * 4); }); const response = yield _this2.transport.send(cla, ins, p1, p2, data); const publicKeyLength = response[0]; const chainCodeOffset = 2 + publicKeyLength; const chainCodeLength = response[1 + publicKeyLength]; return { public_key: response.slice(1, 1 + publicKeyLength).toString("hex"), chain_code: response.slice(chainCodeOffset, chainCodeOffset + chainCodeLength).toString("hex") }; })(); } /** * Sign a Nervos transaction with a given BIP 32 path * * @param signPath the path to sign with, in BIP 32 format * @param rawTxHex transaction to sign * @param groupWitnessesHex hex of in-group and extra witnesses to include in signature * @param contextTransaction list of transaction contexts for parsing * @param changePath the path the transaction sends change to, in BIP 32 format (optional, defaults to signPath) * @return a signature as hex string * @example * TODO */ signTransaction(signPath, rawTx, groupWitnessesHex, rawContextsTx, changePath) { var _this3 = this; return _asyncToGenerator(function* () { return yield _this3.signAnnotatedTransaction(_this3.buildAnnotatedTransaction(signPath, rawTx, groupWitnessesHex, rawContextsTx, changePath)); })(); } /** * Construct an AnnotatedTransaction for a given collection of signing data * * Parameters are the same as for signTransaction, but no ledger interaction is attempted. * * AnnotatedTransaction is a type defined for the ledger app that collects * all of the information needed to securely confirm a transaction on-screen * and a few bits of duplicative information to allow it to be processed as a * stream. */ buildAnnotatedTransaction(signPath, rawTx, groupWitnesses, rawContextsTx, changePath) { const prepBipPath = pathSrc => { if (Array.isArray(pathSrc)) { return pathSrc; } if (typeof pathSrc === "object") { return pathSrc.toPathArray(); } if (typeof pathSrc === "string") { return _bip32Path2.default.fromString(pathSrc).toPathArray(); } }; const signBipPath = prepBipPath(signPath); const changeBipPath = prepBipPath(changePath); const getRawTransactionJSON = rawTrans => { if (typeof rawTrans === "string") { const rawTxBuffer = Buffer.from(rawTrans, "hex"); return new blockchain.RawTransaction(rawTxBuffer.buffer).toObject(); } return rawTrans; }; const contextTransactions = rawContextsTx.map(getRawTransactionJSON); const rawTxUnpacked = getRawTransactionJSON(rawTx); const annotatedCellInputVec = rawTxUnpacked.inputs.map((inpt, idx) => ({ input: inpt, source: contextTransactions[idx] })); const annotatedRawTransaction = { version: rawTxUnpacked.version, cell_deps: rawTxUnpacked.cell_deps, header_deps: rawTxUnpacked.header_deps, inputs: annotatedCellInputVec, outputs: rawTxUnpacked.outputs, outputs_data: rawTxUnpacked.outputs_data }; return { signPath: signBipPath, changePath: changeBipPath, inputCount: rawTxUnpacked.inputs.length, raw: annotatedRawTransaction, witnesses: Array.isArray(groupWitnesses) && groupWitnesses.length > 0 ? groupWitnesses : [this.defaultSighashWitness] }; } /** * Sign an already constructed AnnotatedTransaction. */ signAnnotatedTransaction(tx) { var _this4 = this; return _asyncToGenerator(function* () { const rawAnTx = Buffer.from(blockchain.SerializeAnnotatedTransaction(tx)); const maxApduSize = 230; let txFullChunks = Math.floor(rawAnTx.byteLength / maxApduSize); let isContinuation = 0x00; for (let i = 0; i < txFullChunks; i++) { let data = rawAnTx.slice(i * maxApduSize, (i + 1) * maxApduSize); yield _this4.transport.send(0x80, 0x03, isContinuation, 0x00, data); isContinuation = 0x01; } let lastOffset = txFullChunks * maxApduSize; let lastData = rawAnTx.slice(lastOffset, lastOffset + maxApduSize); let response = yield _this4.transport.send(0x80, 0x03, isContinuation | 0x80, 0x00, lastData); return response.slice(0, 65).toString("hex"); })(); } /** * An empty WitnessArgs with enough space to fit a sighash signature into. */ /** * Get the version of the Nervos app installed on the hardware device * * @return an object with a version * @example * const result = await ckb.getAppConfiguration(); * * { * "version": "1.0.3", * "hash": "0000000000000000000000000000000000000000" * } */ getAppConfiguration() { var _this5 = this; return _asyncToGenerator(function* () { const response1 = yield _this5.transport.send(0x80, 0x00, 0x00, 0x00); const response2 = yield _this5.transport.send(0x80, 0x09, 0x00, 0x00); return { version: "" + response1[0] + "." + response1[1] + "." + response1[2], hash: response2.slice(0, -3).toString("latin1") // last 3 bytes should be 0x009000 }; })(); } /** * Get the wallet identifier for the Ledger wallet * * @return a byte string * @example * const id = await ckb.getWalletId(); * * "0x69c46b6dd072a2693378ef4f5f35dcd82f826dc1fdcc891255db5870f54b06e6" */ getWalletId() { var _this6 = this; return _asyncToGenerator(function* () { const response = yield _this6.transport.send(0x80, 0x01, 0x00, 0x00); const result = response.slice(0, 32).toString("hex"); return result; })(); } signMessage(path, rawMsgHex, displayHex) { var _this7 = this; return _asyncToGenerator(function* () { const bipPath = _bip32Path2.default.fromString(path).toPathArray(); const magicBytes = Buffer.from("Nervos Message:"); const rawMsg = Buffer.concat([magicBytes, Buffer.from(rawMsgHex, "hex")]); //Init apdu let rawPath = Buffer.alloc(1 + 1 + bipPath.length * 4); rawPath.writeInt8(displayHex, 0); rawPath.writeInt8(bipPath.length, 1); bipPath.forEach(function (segment, index) { rawPath.writeUInt32BE(segment, 2 + index * 4); }); yield _this7.transport.send(0x80, 0x06, 0x00, 0x00, rawPath); // Msg Chunking const maxApduSize = 230; let txFullChunks = Math.floor(rawMsg.length / maxApduSize); for (let i = 0; i < txFullChunks; i++) { let data = rawMsg.slice(i * maxApduSize, (i + 1) * maxApduSize); yield _this7.transport.send(0x80, 0x06, 0x01, 0x00, data); } let lastOffset = Math.floor(rawMsg.length / maxApduSize) * maxApduSize; let lastData = rawMsg.slice(lastOffset, lastOffset + maxApduSize); let response = yield _this7.transport.send(0x80, 0x06, 0x81, 0x00, lastData); return response.slice(0, 65).toString("hex"); })(); } } exports.default = Ckb; //# sourceMappingURL=Ckb.js.map