@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
JavaScript
"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