shogun-core
Version:
SHOGUN CORE - Core library for Shogun Ecosystem
421 lines (420 loc) • 17 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NostrConnector = exports.MESSAGE_TO_SIGN = void 0;
exports.deriveNostrKeys = deriveNostrKeys;
/**
* The BitcoinWallet class provides functionality for connecting, signing up, and logging in using Bitcoin wallets.
* Supports Alby and Nostr extensions, as well as manual key management.
*/
const ethers_1 = require("ethers");
const nostr_tools_1 = require("nostr-tools");
const eventEmitter_1 = require("../../utils/eventEmitter");
const derive_1 = __importDefault(require("../../gundb/derive"));
const validation_1 = require("../../utils/validation");
exports.MESSAGE_TO_SIGN = "I Love Shogun!";
/**
* Class for Bitcoin wallet connections and operations
*/
class NostrConnector extends eventEmitter_1.EventEmitter {
DEFAULT_CONFIG = {
cacheDuration: 24 * 60 * 60 * 1000, // 24 hours instead of 30 minutes for better UX
maxRetries: 3,
retryDelay: 1000,
timeout: 60000,
network: "mainnet",
useApi: false,
};
config;
signatureCache = new Map();
// Connection state
connectedAddress = null;
connectedType = null;
manualKeyPair = null;
constructor(config = {}) {
super();
this.config = { ...this.DEFAULT_CONFIG, ...config };
this.setupEventListeners();
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Currently no global events to listen to
// This would be the place to add listeners for wallet connections/disconnections
}
/**
* Clear signature cache for a specific address or all addresses
*/
clearSignatureCache(address) {
if (address) {
// Clear cache for specific address
this.signatureCache.delete(address);
try {
const localStorageKey = `shogun_bitcoin_sig_${address}`;
localStorage.removeItem(localStorageKey);
console.log(`Cleared signature cache for address: ${address.substring(0, 10)}...`);
}
catch (error) {
console.error("Error clearing signature cache from localStorage:", error);
}
}
else {
// Clear all signature caches
this.signatureCache.clear();
try {
// Find and remove all shogun_bitcoin_sig_ keys
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith("shogun_bitcoin_sig_")) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
console.log(`Cleared all signature caches (${keysToRemove.length} entries)`);
}
catch (error) {
console.error("Error clearing all signature caches from localStorage:", error);
}
}
}
/**
* Check if Nostr extension is available
*/
isNostrExtensionAvailable() {
return typeof window !== "undefined" && !!window.nostr;
}
/**
* Check if any Bitcoin wallet is available
*/
isAvailable() {
return this.isNostrExtensionAvailable() || this.manualKeyPair !== null;
}
/**
* Connect to a wallet type
*/
async connectWallet(type = "nostr") {
console.log(`Connecting to Bitcoin wallet via ${type}...`);
try {
let result;
// Attempt to connect to the specified wallet type
switch (type) {
case "alby":
console.log("[nostrConnector] Alby is deprecated, redirecting to Nostr");
result = await this.connectNostr();
break;
case "nostr":
result = await this.connectNostr();
break;
case "manual":
result = await this.connectManual();
break;
default:
throw new Error(`Unsupported wallet type: ${type}`);
}
if (result.success && result.address) {
this.connectedAddress = result.address;
this.connectedType = type;
console.log(`Successfully connected to ${type} wallet: ${result.address}`);
this.emit("wallet_connected", {
address: result.address,
type: this.connectedType,
});
}
return result;
}
catch (error) {
console.error(`Error connecting to ${type} wallet:`, error);
return {
success: false,
error: error.message || "Failed to connect to wallet",
};
}
}
/**
* Connect to Nostr extension
*/
async connectNostr() {
if (!this.isNostrExtensionAvailable()) {
return {
success: false,
error: "Nostr extension is not available. Please install a Nostr compatible extension like nos2x, Alby, or Coracle.",
};
}
try {
console.log("[nostrConnector] Attempting to connect to Nostr extension...");
// Get public key from Nostr extension
const pubKey = await window.nostr.getPublicKey();
if (!pubKey) {
throw new Error("Could not get public key from Nostr extension");
}
console.log(`[nostrConnector] Successfully connected to Nostr extension: ${pubKey.substring(0, 10)}...`);
this.connectedAddress = pubKey;
this.connectedType = "nostr";
// Emit connected event
this.emit("connected", { address: pubKey, type: "nostr" });
const username = `nostr_${pubKey.substring(0, 10)}`;
return {
success: true,
address: pubKey,
username,
extensionType: "nostr",
};
}
catch (error) {
console.error("[nostrConnector] Nostr connection error:", error);
// Provide more specific error messages
if (error.message && error.message.includes("User rejected")) {
throw new Error("Nostr connection was rejected by the user");
}
else if (error.message && error.message.includes("not available")) {
throw new Error("Nostr extension is not available or not properly installed");
}
else {
throw new Error(`Nostr connection error: ${error.message}`);
}
}
}
/**
* Set up manual key pair for connection
*/
async connectManual() {
// For manual connection, we'd need to have a keypair set
if (!this.manualKeyPair) {
return {
success: false,
error: "No manual key pair configured. Use setKeyPair() first.",
};
}
this.connectedAddress = this.manualKeyPair.address;
this.connectedType = "manual";
// Emit connected event
this.emit("connected", {
address: this.manualKeyPair.address,
type: "manual",
});
const username = `btc_${this.manualKeyPair.address.substring(0, 10)}`;
return {
success: true,
address: this.manualKeyPair.address,
username,
extensionType: "manual",
};
}
/**
* Set a manual key pair for use
*/
setKeyPair(keyPair) {
this.manualKeyPair = keyPair;
if (keyPair.address) {
this.connectedAddress = keyPair.address;
this.connectedType = "manual";
}
}
/**
* Generate credentials using Nostr: username deterministico e chiave GunDB derivata dall'address
*/
async generateCredentials(address, signature, message) {
const username = (0, validation_1.generateUsernameFromIdentity)("nostr", { id: address });
// Usa un hashing robusto di address con keccak256
const hashedAddress = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(address));
// Include la signature nel salt per aggiungere un ulteriore livello di sicurezza
const salt = `${username}_${address}_${message}_${signature}`;
const key = await (0, derive_1.default)(hashedAddress, salt, { includeP256: true });
return { username, key, message, signature };
}
/**
* Generate a password from a signature
*/
async generatePassword(signature) {
if (!signature) {
throw new Error("Invalid signature");
}
try {
// Create a deterministic hash from the signature using a secure algorithm
const normalizedSig = signature.toLowerCase().replace(/[^a-f0-9]/g, "");
const passwordHash = ethers_1.ethers.sha256(ethers_1.ethers.toUtf8Bytes(normalizedSig));
return passwordHash;
}
catch (error) {
console.error("Error generating password:", error);
throw new Error("Failed to generate password from signature");
}
}
/**
* Verify a signature
*/
async verifySignature(message, signature, address) {
try {
// Ensure address is a string
const addressStr = typeof address === "object"
? address.address || JSON.stringify(address)
: String(address);
console.log(`Verifying signature for address: ${addressStr}`);
if (!signature || !message || !addressStr) {
console.error("Invalid message, signature, or address for verification");
return false;
}
// For Nostr wallet type, use nostr-tools for verification
if (this.connectedType === "nostr" || this.connectedType === "alby") {
try {
// Reconstruct the exact event that was signed
const eventData = {
kind: 1,
created_at: 0, // IMPORTANT: Use the same fixed timestamp used for signing
tags: [],
content: message,
pubkey: addressStr,
};
const event = {
...eventData,
id: (0, nostr_tools_1.getEventHash)(eventData),
sig: signature,
};
return (0, nostr_tools_1.verifyEvent)(event);
}
catch (verifyError) {
console.error("Error in Nostr signature verification:", verifyError);
return false;
}
}
else if (this.connectedType === "manual" && this.manualKeyPair) {
console.log("[nostrConnector] Manual verification for keypair");
// For manual keypairs, we MUST use a secure verification method.
if (!this.manualKeyPair.privateKey) {
console.error("Manual verification failed: private key is missing.");
return false;
}
try {
const eventData = {
kind: 1,
created_at: 0, // IMPORTANT: Use the same fixed timestamp used for signing
tags: [],
content: message,
pubkey: addressStr,
};
const event = {
...eventData,
id: (0, nostr_tools_1.getEventHash)(eventData),
sig: signature,
};
return (0, nostr_tools_1.verifyEvent)(event);
}
catch (manualVerifyError) {
console.error("Error in manual signature verification:", manualVerifyError);
return false;
}
}
console.warn("No specific verification method available, signature cannot be fully verified");
return false;
}
catch (error) {
console.error("Error verifying signature:", error);
return false;
}
}
/**
* Get the currently connected address
*/
getConnectedAddress() {
return this.connectedAddress;
}
/**
* Get the currently connected wallet type
*/
getConnectedType() {
return this.connectedType;
}
/**
* Request a signature from the connected wallet
*/
async requestSignature(address, message) {
if (!this.connectedType) {
throw new Error("No wallet connected");
}
try {
switch (this.connectedType) {
case "alby":
case "nostr":
if (this.connectedType === "alby") {
console.warn("Alby is deprecated, using Nostr functionality for signature request");
}
console.log("[nostrConnector] Requesting Nostr signature for message:", message);
if (!window.nostr) {
throw new Error("Nostr extension not available");
}
// For Nostr, we need to create an event to sign with a fixed timestamp
const eventData = {
kind: 1,
created_at: 0, // IMPORTANT: Use a fixed timestamp to make signatures verifiable
tags: [],
content: message,
pubkey: address,
};
const nostrEvent = {
...eventData,
id: (0, nostr_tools_1.getEventHash)(eventData),
sig: "", // This will be filled by window.nostr.signEvent
};
const signedEvent = await window.nostr.signEvent(nostrEvent);
console.log("Received Nostr signature:", signedEvent.sig.substring(0, 20) + "...");
return signedEvent.sig;
case "manual":
console.log("[nostrConnector] Using manual key pair for signature");
if (!this.manualKeyPair || !this.manualKeyPair.privateKey) {
throw new Error("No manual key pair available or private key missing");
}
// Use nostr-tools to sign securely
const manualEventData = {
kind: 1,
created_at: 0, // IMPORTANT: Use a fixed timestamp
tags: [],
content: message,
pubkey: this.manualKeyPair.address,
};
const eventTemplate = {
...manualEventData,
id: (0, nostr_tools_1.getEventHash)(manualEventData),
sig: "", // This will be filled by finalizeEvent
};
const privateKeyBytes = nostr_tools_1.utils.hexToBytes(this.manualKeyPair.privateKey);
const signedEventManual = await (0, nostr_tools_1.finalizeEvent)(eventTemplate, privateKeyBytes);
console.log("Generated manual signature:", signedEventManual.sig.substring(0, 20) + "...");
return signedEventManual.sig;
default:
throw new Error(`Unsupported wallet type: ${this.connectedType}`);
}
}
catch (error) {
console.error("Error requesting signature:", error);
throw new Error(`Failed to get signature: ${error.message}`);
}
}
/**
* Cleanup event listeners
*/
cleanup() {
this.removeAllListeners();
this.connectedAddress = null;
this.connectedType = null;
this.manualKeyPair = null;
}
}
exports.NostrConnector = NostrConnector;
// Funzione helper per derivare chiavi Nostr/Bitcoin (come per Web3/WebAuthn)
async function deriveNostrKeys(address, signature, message) {
// Usa solo l'address per rendere le credenziali deterministiche
const salt = `${address}_${message}`;
return await (0, derive_1.default)(address, salt, {
includeP256: true,
});
}
if (typeof window !== "undefined") {
window.NostrConnector = NostrConnector;
}
else if (typeof global !== "undefined") {
global.NostrConnector = NostrConnector;
}