UNPKG

@unisat/keyring-service

Version:

Keyring service for managing Bitcoin wallets - supports HD, simple, and hardware wallets

1,544 lines (1,530 loc) 61.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { ADDRESS_TYPES: () => ADDRESS_TYPES, BrowserPassworderEncryptor: () => BrowserPassworderEncryptor, ColdWalletKeyring: () => ColdWalletKeyring, CosmosSignDataType: () => CosmosSignDataType, ExtensionPersistStoreAdapter: () => ExtensionPersistStoreAdapter, HdKeyring: () => HdKeyring, KeyringService: () => KeyringService, KeyringType: () => KeyringType, KeystoneKeyring: () => KeystoneKeyring, KeystoneSignEnum: () => KeystoneSignEnum, MemoryStorageAdapter: () => MemoryStorageAdapter, SimpleEncryptor: () => SimpleEncryptor, SimpleKeyring: () => SimpleKeyring }); module.exports = __toCommonJS(src_exports); // src/keyring-service.ts var import_events3 = require("events"); var bip392 = __toESM(require("bip39")); var import_obs_store = require("@metamask/obs-store"); // src/types/keyring.ts var KeyringType = /* @__PURE__ */ ((KeyringType2) => { KeyringType2["HdKeyring"] = "HD Key Tree"; KeyringType2["SimpleKeyring"] = "Simple Key Pair"; KeyringType2["KeystoneKeyring"] = "Keystone"; KeyringType2["ColdWalletKeyring"] = "Cold Wallet"; KeyringType2["ReadonlyKeyring"] = "Readonly"; KeyringType2["Empty"] = "Empty"; return KeyringType2; })(KeyringType || {}); // src/types/cosmos.ts var CosmosSignDataType = /* @__PURE__ */ ((CosmosSignDataType2) => { CosmosSignDataType2["amino"] = "amino"; CosmosSignDataType2["direct"] = "direct"; return CosmosSignDataType2; })(CosmosSignDataType || {}); // src/types/keystone.ts var KeystoneSignEnum = /* @__PURE__ */ ((KeystoneSignEnum2) => { KeystoneSignEnum2["MSG"] = "msg"; KeystoneSignEnum2["PSBT"] = "psbt"; KeystoneSignEnum2["BIP322_SIMPLE"] = "bip322-simple"; KeystoneSignEnum2["COSMOS_DIRECT"] = "cosmos-direct"; KeystoneSignEnum2["COSMOS_ARBITRARY"] = "cosmos-arbitrary"; return KeystoneSignEnum2; })(KeystoneSignEnum || {}); // src/keyring-service.ts var import_wallet_types2 = require("@unisat/wallet-types"); // src/encryptor/browser-encryptor.ts var browserPassworder = __toESM(require("browser-passworder")); var BrowserPassworderEncryptor = class { async encrypt(password, data) { return await browserPassworder.encrypt(password, data); } async decrypt(password, encryptedData) { return await browserPassworder.decrypt(password, encryptedData); } }; // src/keyrings/simple-keyring.ts var import_bip371 = require("bitcoinjs-lib/src/psbt/bip371.js"); var import_bs58check = require("bs58check"); var import_events = require("events"); var import_wallet_bitcoin = require("@unisat/wallet-bitcoin"); var type = "Simple Key Pair"; var SimpleKeyring = class extends import_events.EventEmitter { constructor(opts) { super(); this.type = type; this.network = import_wallet_bitcoin.bitcoin.networks.bitcoin; this.wallets = []; if (opts) { this.deserialize(opts); } } async serialize() { return this.wallets.map((wallet) => wallet.privateKey?.toString("hex")); } async deserialize(opts) { const privateKeys = opts; this.wallets = privateKeys.map((key) => { let buf; if (key.length === 64) { buf = Buffer.from(key, "hex"); } else { buf = Buffer.from((0, import_bs58check.decode)(key).slice(1, 33)); } return import_wallet_bitcoin.ECPair.fromPrivateKey(buf); }); } async addAccounts(n = 1) { const newWallets = []; for (let i = 0; i < n; i++) { newWallets.push(import_wallet_bitcoin.ECPair.makeRandom()); } this.wallets = this.wallets.concat(newWallets); const hexWallets = newWallets.map(({ publicKey }) => publicKey.toString("hex")); return hexWallets; } async getAccounts() { return this.wallets.map(({ publicKey }) => publicKey.toString("hex")); } async signTransaction(psbt, inputs, opts) { inputs.forEach((input) => { const keyPair = this._getPrivateKeyFor(input.publicKey); if ((0, import_bip371.isTaprootInput)(psbt.data.inputs[input.index])) { let signer = keyPair; let tweak = true; if (typeof input.useTweakedSigner === "boolean") { tweak = input.useTweakedSigner; } else if (typeof input.disableTweakSigner === "boolean") { tweak = !input.disableTweakSigner; } if (tweak) { signer = (0, import_wallet_bitcoin.tweakSigner)(keyPair, opts); } psbt.signTaprootInput( input.index, signer, input.tapLeafHashToSign, input.sighashTypes ); } else { let signer = keyPair; let tweak = false; if (typeof input.useTweakedSigner === "boolean") { tweak = input.useTweakedSigner; } if (tweak) { signer = (0, import_wallet_bitcoin.tweakSigner)(keyPair, opts); } psbt.signInput(input.index, signer, input.sighashTypes); } }); return psbt; } async signMessage(publicKey, text) { const keyPair = this._getPrivateKeyFor(publicKey); return (0, import_wallet_bitcoin.signMessageOfDeterministicECDSA)(keyPair, text); } async verifyMessage(publicKey, text, sig) { return (0, import_wallet_bitcoin.verifyMessageOfECDSA)(publicKey, text, sig); } // Sign any content, but note that the content signed by this method is unreadable, so use it with caution. async signData(publicKey, data, type4 = "ecdsa") { const keyPair = this._getPrivateKeyFor(publicKey); if (type4 === "ecdsa") { return keyPair.sign(Buffer.from(data, "hex")).toString("hex"); } else if (type4 === "schnorr") { return keyPair.signSchnorr(Buffer.from(data, "hex")).toString("hex"); } else { throw new Error("Not support type"); } } _getPrivateKeyFor(publicKey) { if (!publicKey) { throw new Error("Must specify publicKey."); } const wallet = this._getWalletForAccount(publicKey); return wallet; } async exportAccount(publicKey) { const wallet = this._getWalletForAccount(publicKey); return wallet.privateKey?.toString("hex"); } removeAccount(publicKey) { if (!this.wallets.map((wallet) => wallet.publicKey.toString("hex")).includes(publicKey)) { throw new Error(`PublicKey ${publicKey} not found in this keyring`); } this.wallets = this.wallets.filter((wallet) => wallet.publicKey.toString("hex") !== publicKey); } _getWalletForAccount(publicKey) { let wallet = this.wallets.find((wallet2) => wallet2.publicKey.toString("hex") == publicKey); if (!wallet) { throw new Error("Simple Keyring - Unable to find matching publicKey."); } return wallet; } }; SimpleKeyring.type = type; // src/keyrings/hd-keyring.ts var bip39 = __toESM(require("bip39")); var hdkey = __toESM(require("hdkey")); var import_wallet_bitcoin2 = require("@unisat/wallet-bitcoin"); var hdPathString = "m/44'/0'/0'/0"; var type2 = "HD Key Tree"; var HdKeyring = class extends SimpleKeyring { constructor(opts) { super(null); this.type = type2; this.mnemonic = ""; this.xpriv = ""; this.passphrase = ""; this.network = import_wallet_bitcoin2.bitcoin.networks.bitcoin; this.hdPath = hdPathString; this.root = null; this.wallets = []; this._index2wallet = {}; this.activeIndexes = []; this.page = 0; this.perPage = 5; if (opts) { this.deserialize(opts); } } async serialize() { return { mnemonic: this.mnemonic, xpriv: this.xpriv, activeIndexes: this.activeIndexes, hdPath: this.hdPath, passphrase: this.passphrase }; } async deserialize(_opts = {}) { if (this.root) { throw new Error("Btc-Hd-Keyring: Secret recovery phrase already provided"); } let opts = _opts; this.wallets = []; this.mnemonic = ""; this.xpriv = ""; this.root = null; this.hdPath = opts.hdPath || hdPathString; if (opts.passphrase) { this.passphrase = opts.passphrase; } if (opts.mnemonic) { this.initFromMnemonic(opts.mnemonic); } else if (opts.xpriv) { this.initFromXpriv(opts.xpriv); } if (opts.activeIndexes) { this.activeAccounts(opts.activeIndexes); } } initFromXpriv(xpriv) { if (this.root) { throw new Error("Btc-Hd-Keyring: Secret recovery phrase already provided"); } this.xpriv = xpriv; this._index2wallet = {}; this.hdWallet = hdkey.fromJSON({ xpriv }); this.root = this.hdWallet; } initFromMnemonic(mnemonic) { if (this.root) { throw new Error("Btc-Hd-Keyring: Secret recovery phrase already provided"); } this.mnemonic = mnemonic; this._index2wallet = {}; const seed = bip39.mnemonicToSeedSync(mnemonic, this.passphrase); this.hdWallet = hdkey.fromMasterSeed(seed); this.root = this.hdWallet.derive(this.hdPath); } changeHdPath(hdPath) { if (!this.mnemonic) { throw new Error("Btc-Hd-Keyring: Not support"); } this.hdPath = hdPath; this.root = this.hdWallet.derive(this.hdPath); const indexes = this.activeIndexes; this._index2wallet = {}; this.activeIndexes = []; this.wallets = []; this.activeAccounts(indexes); } getAccountByHdPath(hdPath, index) { if (!this.mnemonic) { throw new Error("Btc-Hd-Keyring: Not support"); } const root = this.hdWallet.derive(hdPath); const child = root.deriveChild(index); const ecpair = import_wallet_bitcoin2.ECPair.fromPrivateKey(child.privateKey, { network: this.network }); const address = ecpair.publicKey.toString("hex"); return address; } addAccounts(numberOfAccounts = 1) { let count = numberOfAccounts; let currentIdx = 0; const newWallets = []; while (count) { const [, wallet] = this._addressFromIndex(currentIdx); if (this.wallets.includes(wallet)) { currentIdx++; } else { this.wallets.push(wallet); newWallets.push(wallet); this.activeIndexes.push(currentIdx); count--; } } const hexWallets = newWallets.map((w) => { return w.publicKey.toString("hex"); }); return Promise.resolve(hexWallets); } activeAccounts(indexes) { const accounts = []; for (const index of indexes) { const [address, wallet] = this._addressFromIndex(index); this.wallets.push(wallet); this.activeIndexes.push(index); accounts.push(address); } return accounts; } getFirstPage() { this.page = 0; return this.__getPage(1); } getNextPage() { return this.__getPage(1); } getPreviousPage() { return this.__getPage(-1); } getAddresses(start, end) { const from = start; const to = end; const accounts = []; for (let i = from; i < to; i++) { const [address] = this._addressFromIndex(i); accounts.push({ address, index: i + 1 }); } return accounts; } async __getPage(increment) { this.page += increment; if (!this.page || this.page <= 0) { this.page = 1; } const from = (this.page - 1) * this.perPage; const to = from + this.perPage; const accounts = []; for (let i = from; i < to; i++) { const [address] = this._addressFromIndex(i); accounts.push({ address, index: i + 1 }); } return accounts; } async getAccounts() { return this.wallets.map((w) => { return w.publicKey.toString("hex"); }); } getIndexByAddress(address) { for (const key in this._index2wallet) { if (this._index2wallet[key]?.[0] === address) { return Number(key); } } return null; } _addressFromIndex(i) { if (!this._index2wallet[i]) { const child = this.root.deriveChild(i); const ecpair = import_wallet_bitcoin2.ECPair.fromPrivateKey(child.privateKey, { network: this.network }); const address = ecpair.publicKey.toString("hex"); this._index2wallet[i] = [address, ecpair]; } return this._index2wallet[i]; } }; HdKeyring.type = type2; // src/keyrings/keystone-keyring.ts var import_keystone_sdk = require("@keystonehq/keystone-sdk"); var import_stringHelper = require("@keystonehq/keystone-sdk/dist/utils/stringHelper.js"); var import_bitcoinjs_lib = require("bitcoinjs-lib"); var import_bitcore_lib = __toESM(require("bitcore-lib")); var import_events2 = require("events"); var import_wallet_bitcoin3 = require("@unisat/wallet-bitcoin"); var type3 = "Keystone"; var DEFAULT_CONNECTION_TYPE = "QR"; var KeystoneKeyring = class extends import_events2.EventEmitter { constructor(opts) { super(); this.type = type3; this.mfp = ""; this.keys = []; this.activeIndexes = []; this.root = null; this.connectionType = "QR"; this.page = 0; this.perPage = 5; this.origin = "UniSat Wallet"; if (opts) { this.deserialize(opts); } } async initFromUR(type4, cbor, connectionType) { const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); const account = keystoneSDK.parseAccount(new import_keystone_sdk.UR(Buffer.from(cbor, "hex"), type4)); await this.deserialize({ mfp: account.masterFingerprint, keys: account.keys.map((k) => ({ path: k.path, extendedPublicKey: k.extendedPublicKey || "" })), connectionType: connectionType ?? DEFAULT_CONNECTION_TYPE }); } getHardenedPath(hdPath) { const paths = hdPath.split("/"); return paths.slice(0, 4).join("/"); } getHDPublicKey(hdPath) { const path = this.getHardenedPath(hdPath); const key = this.keys.find((v) => v.path === path); if (!key) { throw new Error("Invalid path"); } return new import_bitcore_lib.default.HDPublicKey(key.extendedPublicKey); } getDefaultHdPath() { return "m/44'/0'/0'/0"; } getConnectionType() { return this.connectionType ?? DEFAULT_CONNECTION_TYPE; } initRoot() { this.root = this.getHDPublicKey(this.hdPath ?? this.getDefaultHdPath()); } async deserialize(opts) { this.mfp = opts.mfp; this.keys = opts.keys; this.hdPath = opts.hdPath ?? this.getDefaultHdPath(); this.activeIndexes = opts.activeIndexes ? [...opts.activeIndexes] : []; this.connectionType = opts.connectionType ?? DEFAULT_CONNECTION_TYPE; this.initRoot(); if (opts.hdPath !== null && opts.hdPath !== void 0 && opts.hdPath.length >= 13 && opts.hdPath[opts.hdPath.length - 1] === "1") { this.root = this.root.derive(`m/1`); } } async serialize() { return { mfp: this.mfp, keys: this.keys, hdPath: this.hdPath || "", activeIndexes: this.activeIndexes, connectionType: this.connectionType || "QR" }; } async addAccounts(numberOfAccounts = 1) { let count = numberOfAccounts; let i = 0; const pubkeys = []; while (count) { if (this.activeIndexes.includes(i)) { i++; } else { const w = this.getWalletByIndex(i); pubkeys.push(w.publicKey); this.activeIndexes.push(i); count--; } } return Promise.resolve(pubkeys); } async addChangeAddressAccounts(numberOfAccounts = 1) { let count = numberOfAccounts; let i = 0; const pubkeys = []; while (count) { if (this.activeIndexes.includes(i)) { i++; } else { const w = this.getChangeAddressWalletByIndex(i); pubkeys.push(w.publicKey); this.activeIndexes.push(i); count--; } } return Promise.resolve(pubkeys); } async getAccounts() { if (this.hdPath !== null && this.hdPath !== void 0 && this.hdPath.length >= 13 && this.hdPath[this.hdPath.length - 1] === "1") { return this.activeIndexes.map((index) => { const child = this.root.derive(`m/${index}`); return child.publicKey.toString("hex"); }); } return this.activeIndexes.map((i) => this.getWalletByIndex(i).publicKey); } async getAccounts2() { return this.activeIndexes.map((index) => { const child = this.root.derive(`m/${index}`); return { index, path: `${this.hdPath}/${index}`, publicKey: child.publicKey.toString("hex") }; }); } async getAccountsWithBrand() { return this.activeIndexes.map((i) => { const w = this.getWalletByIndex(i); return { address: w.publicKey, index: i }; }); } getWalletByIndex(index) { const child = this.root.derive(`m/0/${index}`); return { index, path: `${this.hdPath}/${index}`, publicKey: child.publicKey.toString("hex") }; } getChangeAddressWalletByIndex(index) { const child = this.root.derive(`m/1/${index}`); return { index, path: `${this.hdPath}/${index}`, publicKey: child.publicKey.toString("hex") }; } removeAccount(publicKey) { const index = this.activeIndexes.findIndex((i) => { const w = this.getWalletByIndex(i); return w.publicKey === publicKey; }); if (index !== -1) { this.activeIndexes.splice(index, 1); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars async exportAccount(_publicKey) { throw new Error("Not supported"); } getFirstPage() { this.page = 0; return this.getPage(1); } getNextPage() { return this.getPage(1); } getPreviousPage() { return this.getPage(-1); } getAddresses(start, end) { const from = start; const to = end; const accounts = []; for (let i = from; i < to; i++) { const w = this.getWalletByIndex(i); accounts.push({ address: w.publicKey, index: i + 1 }); } return accounts; } async getPage(increment) { this.page += increment; if (!this.page || this.page <= 0) { this.page = 1; } const from = (this.page - 1) * this.perPage; const to = from + this.perPage; const accounts = []; for (let i = from; i < to; i++) { const w = this.getWalletByIndex(i); accounts.push({ address: w.publicKey, index: i + 1 }); } return accounts; } activeAccounts(indexes) { const accounts = []; for (const index of indexes) { const w = this.getWalletByIndex(index); if (!this.activeIndexes.includes(index)) { this.activeIndexes.push(index); } accounts.push(w.publicKey); } return accounts; } changeHdPath(hdPath) { this.hdPath = hdPath; this.initRoot(); this.activeAccounts(this.activeIndexes); } changeChangeAddressHdPath(hdPath) { this.hdPath = hdPath; this.root = this.getHDPublicKey(this.hdPath ?? this.getDefaultHdPath()); this.root = this.root.derive(`m/1`); this.activeIndexes = []; return []; } getAccountByHdPath(hdPath, index) { const root = this.getHDPublicKey(hdPath); const child = root.derive(`m/0/${index}`); return child.publicKey.toString("hex"); } getChangeAddressAccountByHdPath(hdPath, index) { const root = this.getHDPublicKey(hdPath); const child = root.derive(`m/1/${index}`); return child.publicKey.toString("hex"); } async genSignPsbtUr(psbtHex) { const psbt = import_bitcoinjs_lib.Psbt.fromHex(psbtHex); const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); const ur = keystoneSDK.btc.generatePSBT(psbt.data.toBuffer()); return { type: ur.type, cbor: ur.cbor.toString("hex") }; } async parseSignPsbtUr(type4, cbor) { const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); return keystoneSDK.btc.parsePSBT(new import_keystone_sdk.UR(Buffer.from(cbor, "hex"), type4)); } async genSignMsgUr(publicKey, text) { const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); let i = void 0; if (this.hdPath !== null && this.hdPath !== void 0 && this.hdPath.length >= 13 && this.hdPath[this.hdPath.length - 1] === "1") { const root = this.getHDPublicKey(this.hdPath); i = this.activeIndexes.find((i2) => { const child = root.derive(`m/1/${i2}`); if (child.publicKey.toString("hex") === publicKey) { return true; } else { return false; } }); } else { i = this.activeIndexes.find((i2) => this.getWalletByIndex(i2).publicKey === publicKey); } if (i === void 0) { throw new Error("publicKey not found"); } const requestId = import_stringHelper.uuid.v4(); const ur = keystoneSDK.btc.generateSignRequest({ requestId, signData: Buffer.from(text).toString("hex"), dataType: import_keystone_sdk.KeystoneBitcoinSDK.DataType.message, accounts: [ { path: `${this.hdPath}/${i}`, xfp: this.mfp } ], origin: this.origin }); return { requestId, type: ur.type, cbor: ur.cbor.toString("hex") }; } async parseSignMsgUr(type4, cbor) { const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); return keystoneSDK.btc.parseSignature(new import_keystone_sdk.UR(Buffer.from(cbor, "hex"), type4)); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async signMessage(publicKey, text) { return "Signing Message with Keystone should use genSignMsgUr and parseSignMsgUr"; } async genSignCosmosUr(cosmosSignRequest) { const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); const requestId = cosmosSignRequest.requestId || import_stringHelper.uuid.v4(); const ur = keystoneSDK.cosmos.generateSignRequest({ requestId, signData: cosmosSignRequest.signData, dataType: cosmosSignRequest.dataType, accounts: [ { path: cosmosSignRequest.path, xfp: this.mfp, address: cosmosSignRequest.address || "" } ], origin: this.origin }); return { requestId, type: ur.type, cbor: ur.cbor.toString("hex") }; } async parseSignCosmosUr(type4, cbor) { const keystoneSDK = new import_keystone_sdk.KeystoneSDK({ origin: this.origin }); return keystoneSDK.cosmos.parseSignature(new import_keystone_sdk.UR(Buffer.from(cbor, "hex"), type4)); } async verifyMessage(publicKey, text, sig) { return (0, import_wallet_bitcoin3.verifyMessageOfECDSA)(publicKey, text, sig); } }; KeystoneKeyring.type = type3; // src/keyrings/cold-wallet-keyring.ts var ColdWalletKeyring = class { constructor(opts) { this.type = "Cold Wallet" /* ColdWalletKeyring */; this.addresses = []; this.connectionType = "QR"; this.hdPath = void 0; this.publicKeys = void 0; this.accounts = void 0; if (!opts) { this.xpub = ""; this.addresses = []; return; } if (!opts.xpub) { throw new Error( "Cold wallet xpub is required. Please re-import your cold wallet with the new format." ); } this.xpub = opts.xpub; this.addresses = opts.addresses || []; this.connectionType = opts.connectionType || "QR"; this.hdPath = opts.hdPath || ""; if (opts.publicKeys) { this.publicKeys = opts.publicKeys; } else if (opts.accounts && Array.isArray(opts.accounts)) { this.publicKeys = opts.accounts.map((acc) => acc.pubkey); } this.accounts = this.publicKeys || []; } async serialize() { return { xpub: this.xpub, addresses: this.addresses, connectionType: this.connectionType, hdPath: this.hdPath || "", publicKeys: this.publicKeys || [] }; } async deserialize(opts) { if (!opts) { throw new Error("Cannot deserialize cold wallet: no data provided"); } this.xpub = opts.xpub || ""; this.addresses = opts.addresses || []; this.connectionType = opts.connectionType || "QR"; this.hdPath = opts.hdPath || ""; if (opts.publicKeys) { this.publicKeys = opts.publicKeys; } else if (opts.accounts && Array.isArray(opts.accounts)) { this.publicKeys = opts.accounts.map((acc) => acc.pubkey); } this.accounts = this.publicKeys || []; } async addAccounts(_n) { throw new Error( "Cold wallet cannot generate accounts. Please import public keys from mobile device." ); } async getAccounts() { if (this.accounts && this.accounts.length > 0) { return this.accounts; } return []; } // Cold wallet cannot sign transactions, need to interact with mobile device via QR code signTransaction(_psbt, _inputs) { throw new Error("Cold wallet cannot sign transactions. Please use mobile device to sign."); } signMessage(_address, _message) { throw new Error("Cold wallet cannot sign messages. Please use mobile device to sign."); } signData(_address, _data, _type) { throw new Error("Cold wallet cannot sign data. Please use mobile device to sign."); } verifyMessage(_address, _message, _sig) { throw new Error("Cold wallet cannot verify messages."); } exportAccount(_address) { throw new Error("Cold wallet cannot export private keys."); } removeAccount(address) { const index = this.addresses.indexOf(address); if (index !== -1) { this.addresses.splice(index, 1); } } // Generate a QR code for signing PSBT async genSignPsbtUr(psbtHex) { return { type: "crypto-psbt", cbor: Buffer.from(psbtHex, "hex").toString("base64") }; } // Parse the signed PSBT returned from the mobile device async parseSignPsbtUr(_type, cbor) { return Buffer.from(cbor, "base64").toString("hex"); } // Generate a QR code for signing messages async genSignMsgUr(publicKey, text) { const requestId = Date.now().toString(); return { type: "crypto-message", cbor: Buffer.from(JSON.stringify({ publicKey, text, requestId })).toString("base64"), requestId }; } // Parse the signed message returned from the mobile device async parseSignMsgUr(_type, cbor) { const data = JSON.parse(Buffer.from(cbor, "base64").toString()); return { requestId: data.requestId, publicKey: data.publicKey, signature: data.signature }; } getConnectionType() { return "QR"; } async genSignCosmosUr(cosmosSignRequest) { const requestId = cosmosSignRequest.requestId || Date.now().toString(); return { type: "crypto-cosmos", cbor: Buffer.from(JSON.stringify(cosmosSignRequest)).toString("base64"), requestId }; } async parseSignCosmosUr(_type, cbor) { return JSON.parse(Buffer.from(cbor, "base64").toString()); } addAddress(address) { if (!this.addresses.includes(address)) { this.addresses.push(address); } } // Check if the address exists hasAddress(address) { return this.addresses.includes(address); } }; // src/constant.ts var import_wallet_types = require("@unisat/wallet-types"); var ADDRESS_TYPES = [ { value: import_wallet_types.AddressType.P2PKH, label: "P2PKH", name: "Legacy (P2PKH)", hdPath: "m/44'/0'/0'/0", displayIndex: 3, isUnisatLegacy: false }, { value: import_wallet_types.AddressType.P2WPKH, label: "P2WPKH", name: "Native Segwit (P2WPKH)", hdPath: "m/84'/0'/0'/0", displayIndex: 0, isUnisatLegacy: false }, { value: import_wallet_types.AddressType.P2TR, label: "P2TR", name: "Taproot (P2TR)", hdPath: "m/86'/0'/0'/0", displayIndex: 2, isUnisatLegacy: false }, { value: import_wallet_types.AddressType.P2SH_P2WPKH, label: "P2SH-P2WPKH", name: "Nested Segwit (P2SH-P2WPKH)", hdPath: "m/49'/0'/0'/0", displayIndex: 1, isUnisatLegacy: false }, { value: import_wallet_types.AddressType.M44_P2WPKH, label: "P2WPKH", name: "Native SegWit (P2WPKH)", hdPath: "m/44'/0'/0'/0", displayIndex: 4, isUnisatLegacy: true }, { value: import_wallet_types.AddressType.M44_P2TR, label: "P2TR", name: "Taproot (P2TR)", hdPath: "m/44'/0'/0'/0", displayIndex: 5, isUnisatLegacy: true } ]; // src/keyrings/empty-keyring.ts var EmptyKeyring = class { constructor() { this.type = "Empty" /* Empty */; } async addAccounts(n) { return []; } async getAccounts() { return []; } signTransaction(psbt, inputs) { throw new Error("Method not implemented in empty keyring."); } signMessage(address, message) { throw new Error("Method not implemented in empty keyring."); } verifyMessage(address, message, sig) { throw new Error("Method not implemented in empty keyring."); } signData(address, data, type4) { throw new Error("Method not implemented in empty keyring."); } exportAccount(address) { throw new Error("Method not implemented in empty keyring."); } removeAccount(address) { throw new Error("Method not implemented in empty keyring."); } async serialize() { return {}; } async deserialize(opts) { } }; // src/keyring-service.ts var EVENTS = { broadcastToUI: "broadcastToUI", broadcastToBackground: "broadcastToBackground", SIGN_FINISHED: "SIGN_FINISHED", WALLETCONNECT: { STATUS_CHANGED: "WALLETCONNECT_STATUS_CHANGED", INIT: "WALLETCONNECT_INIT", INITED: "WALLETCONNECT_INITED" } }; var KEYRING_SDK_TYPES = { SimpleKeyring, HdKeyring, KeystoneKeyring, ColdWalletKeyring }; var DisplayKeyring = class { constructor(keyring) { this.accounts = []; this.type = ""; this.hdPath = ""; this.accounts = keyring.accounts || []; this.type = keyring.type; this.hdPath = keyring.hdPath; } }; var KeyringService = class extends import_events3.EventEmitter { constructor(config) { super(); // Core state - aligned with unisat-extension this.keyringTypes = []; this.keyrings = []; this.addressTypes = []; this.password = null; this.isUnlocking = false; this.cachedDisplayedKeyring = null; this.loadStore = (initState) => { this.store = new import_obs_store.ObservableStore(initState); }; this.boot = async (password) => { this.password = password; const encryptBooted = await this.encryptor.encrypt(password, "true"); if (!this.store) { this.loadStore({ isUnlocked: false, keyrings: [], keyringTypes: [], preMnemonics: "", addressTypes: [] }); } this.store.updateState({ booted: encryptBooted }); await this.updateStore({ booted: encryptBooted }); this.setUnlocked(); this.fullUpdate(); }; this.isBooted = () => { return !!this.store.getState().booted; }; this.hasVault = () => { return !!this.store.getState().vault; }; /** * Full Update * * Emits the `update` event and @returns a Promise that resolves to * the current state. * * Frequently used to end asynchronous chains in this class, * indicating consumers can often either listen for updates, * or accept a state-resolving promise to consume their results. * * @returns {Object} The controller state. */ this.fullUpdate = () => { this.emit("update", this.memStore.getState()); return this.memStore.getState(); }; /** * Import Keychain using Private key * * @emits KeyringController#unlock * @param privateKey - The privateKey to generate address * @returns A Promise that resolves to the state. */ this.importPrivateKey = async (privateKey, addressType) => { const keyring = await this.addNewKeyring("Simple Key Pair", [privateKey], addressType); this.setUnlocked(); this.fullUpdate(); return keyring; }; this.importPublicKeyOnly = async (pubkey, addressType) => { const keyring = await this.addNewKeyring("Readonly", [pubkey], addressType); this.setUnlocked(); this.fullUpdate(); return keyring; }; this.generateMnemonic = () => { return bip392.generateMnemonic(128); }; this.generatePreMnemonic = async () => { if (!this.password) { throw new Error(this.t("you_need_to_unlock_wallet_first")); } const mnemonic = this.generateMnemonic(); const preMnemonics = await this.encryptor.encrypt(this.password, mnemonic); this.memStore.updateState({ preMnemonics }); return mnemonic; }; this.getKeyringByType = (type4) => { const keyring = this.keyrings.find((keyring2) => keyring2.type === type4); return keyring; }; this.removePreMnemonics = () => { this.memStore.updateState({ preMnemonics: "" }); }; this.getPreMnemonics = async () => { if (!this.memStore.getState().preMnemonics) { return ""; } if (!this.password) { throw new Error(this.t("you_need_to_unlock_wallet_first")); } return await this.encryptor.decrypt(this.password, this.memStore.getState().preMnemonics); }; // Alias for compatibility this.getPreMnemonic = this.getPreMnemonics; /** * CreateNewVaultAndRestore Mnenoic * * Destroys any old encrypted storage, * creates a new HD wallet from the given seed with 1 account. * * @emits KeyringController#unlock * @param seed - The BIP44-compliant seed phrase. * @returns A Promise that resolves to the state. */ this.createKeyringWithMnemonics = async (seed, hdPath, passphrase, addressType, accountCount) => { if (accountCount < 1) { throw new Error(this.t("account_count_must_be_greater_than_0")); } if (!bip392.validateMnemonic(seed)) { return Promise.reject(new Error(this.t("mnemonic_phrase_is_invalid"))); } const activeIndexes = []; for (let i = 0; i < accountCount; i++) { activeIndexes.push(i); } const keyring = await this.addNewKeyring( "HD Key Tree", { mnemonic: seed, activeIndexes, hdPath, passphrase }, addressType ); const accounts = await keyring.getAccounts(); if (!accounts[0]) { throw new Error(this.t("first_account_not_found")); } this.setUnlocked(); this.fullUpdate(); return keyring; }; this.createKeyringWithKeystone = async (urType, urCbor, addressType, hdPath, accountCount, connectionType = "USB") => { if (accountCount < 1) { throw new Error(this.t("account_count_must_be_greater_than_0")); } const tmpKeyring = new KeystoneKeyring(); await tmpKeyring.initFromUR(urType, urCbor, connectionType); if (hdPath.length >= 13) { tmpKeyring.changeChangeAddressHdPath(hdPath); tmpKeyring.addAccounts(accountCount); } else { const typeConfig = ADDRESS_TYPES[addressType]; tmpKeyring.changeHdPath(typeConfig ? typeConfig.hdPath : ""); tmpKeyring.addAccounts(accountCount); } const opts = await tmpKeyring.serialize(); const keyring = await this.addNewKeyring("Keystone" /* KeystoneKeyring */, opts, addressType); const accounts = await keyring.getAccounts(); if (!accounts[0]) { throw new Error(this.t("keyringcontroller_first_account_not_found")); } this.setUnlocked(); return keyring; }; this.addKeyring = async (keyring, addressType) => { const accounts = await keyring.getAccounts(); await this.checkForDuplicate(keyring.type, accounts); this.keyrings.push(keyring); this.addressTypes.push(addressType); this.cachedDisplayedKeyring = null; await this.persistAllKeyrings(); await this._updateMemStoreKeyrings(); await this.fullUpdate(); return keyring; }; this.changeAddressType = async (keyringIndex, addressType) => { const keyring = this.keyrings[keyringIndex]; if (keyring.type === "HD Key Tree" /* HdKeyring */ || keyring.type === "Keystone" /* KeystoneKeyring */) { const hdPath = ADDRESS_TYPES[addressType]?.hdPath; if (keyring.hdPath !== hdPath && keyring.changeHdPath) { keyring.changeHdPath(hdPath || ""); } } this.addressTypes[keyringIndex] = addressType; this.cachedDisplayedKeyring = null; await this.persistAllKeyrings(); await this._updateMemStoreKeyrings(); await this.fullUpdate(); return keyring; }; /** * Set Locked * This method deallocates all secrets, and effectively locks MetaMask. * * @emits KeyringController#lock * @returns {Promise<Object>} A Promise that resolves to the state. */ this.setLocked = async () => { this.password = null; this.memStore.updateState({ isUnlocked: false }); this.keyrings = []; this.addressTypes = []; this.cachedDisplayedKeyring = null; await this._updateMemStoreKeyrings(); this.emit("lock"); return this.fullUpdate(); }; /** * Submit Password * * Attempts to decrypt the current vault and load its keyrings * into memory. * * Temporarily also migrates any old-style vaults first, as well. * (Pre MetaMask 3.0.0) * * @emits KeyringController#unlock * @param {string} password - The keyring controller password. * @returns {Promise<Object>} A Promise that resolves to the state. */ this.submitPassword = async (password) => { if (this.isUnlocking) { throw new Error(this.t("unlock_already_in_progress")); } this.isUnlocking = true; try { const isValidPassword = await this.verifyPassword(password); if (!isValidPassword) { throw new Error(this.t("invalid_password")); } this.password = password; this.keyrings = await this.unlockKeyrings(password); this.cachedDisplayedKeyring = null; this.setUnlocked(); return this.fullUpdate(); } catch (e) { throw e; } finally { this.isUnlocking = false; } }; this.changePassword = async (oldPassword, newPassword) => { try { if (this.isUnlocking) { throw new Error(this.t("change_password_already_in_progress")); } this.isUnlocking = true; const isValidPassword = await this.verifyPassword(oldPassword); if (!isValidPassword) { throw new Error(this.t("invalid_password")); } await this.unlockKeyrings(oldPassword); this.password = newPassword; const encryptBooted = await this.encryptor.encrypt(newPassword, "true"); this.store.updateState({ booted: encryptBooted }); if (this.memStore.getState().preMnemonics) { const mnemonic = await this.encryptor.decrypt( oldPassword, this.memStore.getState().preMnemonics ); const preMnemonics = await this.encryptor.encrypt(newPassword, mnemonic); this.memStore.updateState({ preMnemonics }); } await this.persistAllKeyrings(); await this._updateMemStoreKeyrings(); await this.fullUpdate(); } catch (e) { throw new Error(this.t("change_password_failed")); } finally { this.isUnlocking = false; } }; /** * Verify Password * * Attempts to decrypt the current vault with a given password * to verify its validity. * * @param {string} password */ this.verifyPassword = async (password) => { const encryptedBooted = this.store.getState().booted; if (!encryptedBooted) { throw new Error(this.t("cannot_unlock_without_a_previous_vault")); } try { await this.encryptor.decrypt(password, encryptedBooted); return true; } catch { return false; } }; /** * Add New Keyring * * Adds a new Keyring of the given `type` to the vault * and the current decrypted Keyrings array. * * All Keyring classes implement a unique `type` string, * and this is used to retrieve them from the keyringTypes array. * * @param type - The type of keyring to add. * @param opts - The constructor options for the keyring. * @returns The new keyring. */ this.addNewKeyring = async (type4, opts, addressType) => { const Keyring2 = this.getKeyringClassForType(type4); const keyring = new Keyring2(opts); return await this.addKeyring(keyring, addressType); }; this.createTmpKeyring = (type4, opts) => { if (type4 === "Cold Wallet" /* ColdWalletKeyring */) { return new ColdWalletKeyring(opts); } const Keyring2 = this.getKeyringClassForType(type4); if (!Keyring2) { throw new Error(`Unknown keyring type: ${type4}`); } const keyring = new Keyring2(opts); return keyring; }; /** * Checks for duplicate keypairs, using the the first account in the given * array. Rejects if a duplicate is found. * * Only supports 'Simple Key Pair'. * * @param {string} type - The key pair type to check for. * @param {Array<string>} newAccountArray - Array of new accounts. * @returns {Promise<Array<string>>} The account, if no duplicate is found. */ this.checkForDuplicate = async (type4, newAccountArray) => { const keyrings = this.getKeyringsByType(type4); const _accounts = await Promise.all(keyrings.map((keyring) => keyring.getAccounts())); const accounts = _accounts.reduce((m, n) => m.concat(n), []); const isIncluded = newAccountArray.some((account) => { return accounts.find((key) => key === account); }); return isIncluded ? Promise.reject(new Error(this.t("wallet_existed"))) : Promise.resolve(newAccountArray); }; /** * Add New Account * * Calls the `addAccounts` method on the given keyring, * and then saves those changes. * * @param {Keyring} selectedKeyring - The currently selected keyring. * @returns {Promise<Object>} A Promise that resolves to the state. */ this.addNewAccount = async (selectedKeyring) => { const accounts = await selectedKeyring.addAccounts(1); this.cachedDisplayedKeyring = null; accounts.forEach((hexAccount) => { this.emit("newAccount", hexAccount); }); await this.persistAllKeyrings(); await this._updateMemStoreKeyrings(); await this.fullUpdate(); return accounts; }; /** * Export Account * * Requests the private key from the keyring controlling * the specified address. * * Returns a Promise that may resolve with the private key string. * * @param {string} address - The address of the account to export. * @returns {Promise<string>} The private key of the account. */ this.exportAccount = async (address) => { const keyring = await this.getKeyringForAccount(address); const privkey = await keyring.exportAccount(address); return privkey; }; /** * * Remove Account * * Removes a specific account from a keyring * If the account is the last/only one then it also removes the keyring. * * @param {string} address - The address of the account to remove. * @returns {Promise<void>} A Promise that resolves if the operation was successful. */ this.removeAccount = async (address, type4, brand) => { const keyring = await this.getKeyringForAccount(address, type4); if (typeof keyring.removeAccount != "function") { throw new Error( `Keyring ${keyring.type} ${this.t("does_not_support_account_removal_operations")}` ); } keyring.removeAccount(address); this.cachedDisplayedKeyring = null; this.emit("removedAccount", address); await this.persistAllKeyrings(); await this._updateMemStoreKeyrings(); await this.fullUpdate(); }; this.removeKeyring = async (keyringIndex) => { delete this.keyrings[keyringIndex]; this.keyrings[keyringIndex] = new EmptyKeyring(); this.cachedDisplayedKeyring = null; await this.persistAllKeyrings(); await this._updateMemStoreKeyrings(); await this.fullUpdate(); }; // // SIGNING METHODS // /** * Sign BTC Transaction * * Signs an BTC transaction object. * * @param btcTx - The transaction to sign. * @param fromAddress - The transaction 'from' address. * @returns The signed transactio object. */ this.signTransaction = (keyring, psbt, inputs) => { return keyring.signTransaction(psbt, inputs); }; /** * Sign Message * * Attempts to sign the provided message parameters. */ this.signMessage = async (address, keyringType, data) => { const keyring = await this.getKeyringForAccount(address, keyringType); const sig = await keyring.signMessage(address, data); return sig; }; /** * Decrypt Message * * Attempts to verify the provided message parameters. */ this.verifyMessage = async (address, data, sig) => { const keyring = await this.getKeyringForAccount(address); const result = await keyring.verifyMessage(address, data, sig); return result; }; /** * Sign Data * * Sign any content, but note that the content signed by this method is unreadable, so use it with caution. * */ this.signData = async (address, data, type4) => { const keyring = await this.getKeyringForAccount(address); const result = await keyring.signData(address, data, type4); return result; }; // // PRIVATE METHODS // /** * Persist All Keyrings * * Iterates the current `keyrings` array, * serializes each one into a serialized array, * encrypts that array with the provided `password`, * and persists that encrypted string to storage. * * @param {string} password - The keyring controller password. * @returns {Promise<boolean>} Resolves to true once keyrings are persisted. */ this.persistAllKeyrings = () => { if (!this.password || typeof this.password !== "string") { return Promise.reject(new Error(this.t("keyringcontroller_password_is_not_a_string"))); } return Promise.all( this.keyrings.map((keyring, index) => { return Promise.all([keyring.type, keyring.serialize()]).then((serializedKeyringArray) => { return { type: serializedKeyringArray[0], data: serializedKeyringArray[1], addressType: this.addressTypes[index] }; }); }) ).then((serializedKeyrings) => { return this.encryptor.encrypt( this.password, serializedKeyrings ); }).then((encryptedString) => { this.store.updateState({ vault: encryptedString }); return true; }); }; /** * Unlock Keyrings * * Attempts to unlock the persisted encrypted storage, * initializing the persisted keyrings to RAM. * * @param {string} password - The keyring controller password. * @returns {Promise<Array<Keyring>>} The keyrings. */ this.unlockKeyrings = async (password) => { const encryptedVault = this.store.getState().vault; if (!encryptedVault) { throw new Error(this.t("cannot_unlock_without_a_previous_vault")); } await this.clearKeyrings(); const vault = await this.encryptor.decrypt(password, encryptedVault); const arr = Array.from(vault); for (let i = 0; i < arr.length; i++) { const { keyring, addressType } = await this._restoreKeyring(arr[i]); this.keyrings.push(keyring); this.addressTypes.push(addressType); } this.cachedDisplayedKeyring = null; await this._updateMemStoreKeyrings(); return this.keyrings; }; /** * Restore Keyring * * Attempts to initialize a new keyring from the provided serialized payload. * On success, updates the memStore keyrings and returns the resulting * keyring instance. * * @param {Object} serialized - The serialized keyring. * @returns {Promise<Keyring>} The deserialized keyring. */ this.restoreKeyring = async (serialized) => { const { keyring } = await this._restoreKeyring(seri