cyphertap
Version:
Nostr, Lightning and Ecash on a single Button component
913 lines (912 loc) • 37 kB
JavaScript
// src/lib/client/stores/wallet.ts
import { writable, get, derived } from 'svelte/store';
import { consolidateMintTokens, NDKCashuWallet } from '@nostr-dev-kit/ndk-wallet';
import { NDKCashuMintList } from '@nostr-dev-kit/ndk';
import { NDKRelaySet, NDKSubscriptionCacheUsage, NDKCashuWalletTx, NDKKind, NDKEvent } from '@nostr-dev-kit/ndk';
import { getEncodedTokenV4 } from '@cashu/cashu-ts';
import { getNDK, ndkInstance } from './nostr.js';
import { validateMint } from '../utils/validateMint.js';
import { createDebug } from '../utils/debug.js';
// Create a debug logger for the wallet module
const d = createDebug('wallet');
const debug = createDebug('wallet');
// Create specific loggers for sub-components
const dTx = d.extend('tx');
const dMint = d.extend('mint');
const dToken = d.extend('token');
// Wallet state
export const wallet = writable(undefined);
export const walletBalance = writable(0);
export const walletTransactions = writable([]);
export const isWalletReady = writable(false);
export const isInitializingWallet = writable(false);
export const isLoadingTransactions = writable(false);
export const DEFAULT_MINTS = ['https://mint.cypherflow.ai'];
export const REQUIRED_DEPOSIT_AMOUNT = 1; // in sats
// Keep track of transaction IDs to avoid duplicates
let knownTransactionIds = new Set();
let txSubscription = null;
// Initialize wallet for the current user
export async function initWallet(isNewUser = false) {
const d = debug.extend('initWallet');
const dEvent = debug.extend('event');
let ndk = getNDK();
d.log('Starting NIP-60 wallet...');
isInitializingWallet.set(true);
let userWallet;
const poolRelaySet = new NDKRelaySet(new Set(ndk.pool.relays.values()), ndk);
// const explicitRelaySet = NDKRelaySet.fromRelayUrls(ndk.explicitRelayUrls, ndk)
try {
if (isNewUser) {
// For new quick start users, set up wallet with default mints
userWallet = await setupCashuWallet(DEFAULT_MINTS, poolRelaySet);
}
else {
// For everyone else, check if they already have a nip-60 wallet
userWallet = await findExistingWallet(poolRelaySet);
if (userWallet) {
d.log("Found existing NIP-60 wallet");
}
else {
userWallet = await setupCashuWallet(DEFAULT_MINTS, poolRelaySet);
}
}
wallet.set(userWallet);
// Update balance store when wallet reports balance changes
userWallet.on('balance_updated', () => {
const balance = userWallet?.balance;
dEvent.log('Balance updated:', balance);
walletBalance.set(balance?.amount || 0);
});
// Listen for wallet warnings
userWallet.on('warning', (warning) => {
dEvent.warn('Client wallet warning:', warning.msg);
});
// Setup mint-related event handlers
userWallet.onMintInfoNeeded = async (mint) => {
dMint.log(`Fetching info for mint: ${mint}`);
try {
// You can implement custom mint info fetching here if needed
return undefined; // Let the default behavior handle it
}
catch (error) {
dMint.error(`Error fetching mint info for ${mint}:`, error);
return undefined;
}
};
userWallet.onMintInfoLoaded = (mint, info) => {
dMint.log(`Successfully loaded mint info for ${mint}:`, info.version || "Unknown version");
};
userWallet.onMintKeysNeeded = async (mint) => {
dMint.log(`Fetching keys for mint: ${mint}`);
try {
return undefined; // Default behavior
}
catch (error) {
dMint.error(`Error fetching mint keys for ${mint}:`, error);
return undefined;
}
};
userWallet.onMintKeysLoaded = (mint, keysets) => {
dMint.log(`Successfully loaded mint keys for ${mint}, ${keysets.size} keysets loaded`);
};
// Listen for wallet events
userWallet.on('ready', async () => {
// check if tokens are avalid after getting them from relays
d.log('Consolidating tokens...');
await consolidateTokens();
d.log('✅ Wallet is ready');
isWalletReady.set(true);
const balance = userWallet?.balance;
d.log('Initialized balance:', balance);
walletBalance.set(balance?.amount || 0);
// Pre-load wallet connections to each mint
if (userWallet && userWallet.mints) {
for (const mint of userWallet.mints) {
try {
dMint.log(`Testing connection to mint: ${mint}`);
await userWallet.getCashuWallet(mint);
dMint.log(`✅ Successfully connected to mint: ${mint}`);
}
catch (error) {
dMint.warn(`Warning: Could not connect to mint ${mint}:`, error);
// We don't fail here, just log the warning
}
}
}
// Load transaction history when wallet is ready
d.log('Loading transaction history...');
await loadTransactionHistory();
// Start listening for new transactions
d.log('Setting up transaction subscription...');
setupTransactionSubscription();
});
// Start monitoring the wallet
d.log('Starting wallet monitoring...');
userWallet?.start({
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY
});
d.log('✅ Wallet initialization complete');
return userWallet;
}
catch (error) {
d.error('❌ Failed to initialize wallet:', error);
throw error;
}
finally {
isInitializingWallet.set(false);
}
}
async function findExistingWallet(relaySet) {
const d = debug.extend('findExistingWallet');
d.log('Looking for existing wallet...');
const ndk = getNDK();
const activeUser = ndk.activeUser;
if (!activeUser) {
d.error('No active user found, cannot find wallet');
throw 'we need a user first, set a signer in ndk';
}
// check relays directly
d.log("using ndk pool set: ", relaySet);
d.log(`Fetching wallet events for user ${activeUser.pubkey.slice(0, 8)}...`);
let event = await ndk.fetchEvent({ kinds: [NDKKind.CashuWallet], authors: [activeUser.pubkey] }, { cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, subId: 'wallet-sub' }, relaySet);
if (event) {
d.log("Found existing wallet in relays");
return await NDKCashuWallet.from(event);
}
d.log("No existing wallet found");
return undefined;
}
async function setupCashuWallet(mints, relaySet) {
const d = debug.extend('setupCashuWallet');
d.log(`Setting up new Cashu wallet with mints: ${mints.join(', ')}`);
const ndk = getNDK();
// Create the Cashu wallet instance
const wallet = new NDKCashuWallet(ndk);
// Add mints to the wallet
wallet.mints = mints;
d.log("Using relaySet: ", relaySet);
wallet.relaySet = relaySet;
// Generate or load a p2pk (Pay-to-Public-Key) token
// This is used for receiving payments with NIP-61 (nutzaps)
d.log('Generating p2pk token...');
await wallet.getP2pk();
d.log(`Wallet p2pk generated`);
d.log('Publishing wallet event...');
await wallet.publish();
d.log('✅ Published wallet event');
// configure reception of NIP-61 nutzaps for the user
// this publishes an event that tells others who want to zap
// this user the information they need to publish a NIP-61 nutzap.
d.log('Configuring nutzap reception...');
const mintlistForNutzapReception = new NDKCashuMintList(ndk);
mintlistForNutzapReception.relays = wallet.relaySet.relayUrls;
mintlistForNutzapReception.mints = wallet.mints;
mintlistForNutzapReception.p2pk = wallet.p2pk;
await mintlistForNutzapReception.publish();
d.log('✅ Published nutzap mintlist event');
return wallet;
}
// Format transaction for display
export function formatTransaction(tx) {
return {
id: tx.id,
amount: tx.amount ? Number(tx.amount) : 0,
direction: tx.direction || 'unknown',
timestamp: tx.created_at || 0,
date: tx.created_at ? new Date(tx.created_at * 1000) : new Date(),
description: tx.description || 'No description',
fee: tx.fee ? Number(tx.fee) : 0,
mint: tx.mint || 'Unknown',
hasNutzapRedemption: tx.hasNutzapRedemption || false,
event: tx
};
}
// Set up subscription for new transaction events
function setupTransactionSubscription() {
// Clean up any existing subscription
if (txSubscription) {
dTx.log('Stopping existing transaction subscription');
txSubscription.stop();
txSubscription = null;
}
const ndk = getNDK();
const activeUser = ndk.activeUser;
if (!activeUser) {
dTx.error('Cannot set up transaction subscription: no active user');
return;
}
dTx.log("Setting up transaction subscription");
txSubscription = ndk.subscribe({
kinds: [NDKKind.CashuWalletTx],
authors: [activeUser.pubkey],
since: Math.floor(Date.now() / 1000) - 5 * 60 // Last 5 minutes to avoid missing recent transactions
}, {
closeOnEose: false,
subId: 'txs-sub'
});
txSubscription.on('event', async (event) => {
// Skip if there is no IDs
if (!event.id) {
dTx.warn('Received transaction event with no ID, skipping');
return;
}
// Skip if we already know about this transaction
if (knownTransactionIds.has(event.id)) {
dTx.log(`Skipping already known transaction: ${event.id.slice(0, 8)}`);
return;
}
try {
dTx.log(`Processing new transaction event: ${event.id.slice(0, 8)}`);
const tx = await NDKCashuWalletTx.from(event);
if (tx && tx.id) {
// Add to known transactions
knownTransactionIds.add(event.id);
// Update the transactions list
walletTransactions.update((txs) => {
// Dont add if we already have this tx by id
if (txs.some((t) => t.id === tx.id)) {
dTx.log(`Transaction ${tx.id?.slice(0, 8)} already in list, not adding`);
return txs;
}
// Insert at beginning for newest first
dTx.log(`Added new transaction to list: ${tx.id?.slice(0, 8)}`);
return [tx, ...txs];
});
}
}
catch (error) {
dTx.error('Error parsing new transaction:', error);
}
});
dTx.log('✅ Transaction subscription set up successfully');
}
// Load transaction history
export async function loadTransactionHistory() {
const ndk = getNDK();
const activeUser = ndk.activeUser;
if (!activeUser) {
dTx.error('No active user, cannot load transaction history');
throw 'we need a user first, set a signer in ndk';
}
try {
isLoadingTransactions.set(true);
dTx.log("Loading transaction history...");
// Reset state for a clean fetch
knownTransactionIds = new Set();
walletTransactions.set([]);
// Fetch transaction events
dTx.log(`Fetching transaction events for user ${activeUser.pubkey.slice(0, 8)}`);
const txEvents = await ndk.fetchEvents({
kinds: [NDKKind.CashuWalletTx],
authors: [activeUser.pubkey],
// You might want to limit this to recent transactions initially
limit: 50
});
dTx.log(`Found ${txEvents.size} transaction events`);
// Process and convert events to NDKCashuWalletTx objects
const txs = [];
for (const event of txEvents) {
try {
// Skip if there is no IDs
if (!event.id) {
dTx.warn('Skipping event transaction with no ID');
continue;
}
// Skip if we've already processed this transaction
if (knownTransactionIds.has(event.id)) {
dTx.log(`Skipping duplicate transaction ID: ${event.id.slice(0, 8)}`);
continue;
}
dTx.log(`Processing transaction: ${event.id.slice(0, 8)}`);
const tx = await NDKCashuWalletTx.from(event);
if (tx) {
// Add to our known transaction IDs
knownTransactionIds.add(event.id);
txs.push(tx);
}
}
catch (error) {
dTx.error('Error parsing transaction:', error);
}
}
// Sort transactions by created_at (newest first)
txs.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
// Update the store
walletTransactions.set(txs);
dTx.log(`✅ Transaction history loaded: ${txs.length} transactions`);
// Restart the transaction subscription with the latest timestamp
// Only if we have a wallet ready
if (get(isWalletReady) && getNDK()) {
dTx.log('Restarting transaction subscription');
setupTransactionSubscription();
}
}
catch (error) {
dTx.error('Failed to load transaction history:', error);
}
finally {
isLoadingTransactions.set(false);
}
}
// Deposit function (to create a Lightning invoice)
export async function createDeposit(amount) {
d.log(`Creating deposit for ${amount} sats`);
const currentWallet = get(wallet);
if (!currentWallet) {
d.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
// Get the main mint from the mint store
const currentMainMint = get(mainMint);
if (!currentMainMint) {
d.error('No main mint configured');
throw new Error('No main mint configured');
}
// Create the deposit using the main mint
d.log(`Using mint: ${currentMainMint}`);
const deposit = currentWallet.deposit(amount, currentMainMint);
// Set up deposit monitoring
deposit.on('success', (token) => {
d.log('✅ Deposit successful:', token);
});
deposit.on('error', (error) => {
d.error('❌ Deposit failed:', error);
});
// Start the deposit process to get the invoice
d.log('Starting deposit process...');
const bolt11 = await deposit.start();
d.log(`Generated invoice: ${bolt11.substring(0, 20)}...`);
return {
bolt11,
deposit
};
}
// Function to send sats via Lightning
export async function sendLNPayment(bolt11) {
d.log(`Sending LN payment for invoice: ${bolt11.substring(0, 20)}...`);
const currentWallet = get(wallet);
if (!currentWallet) {
d.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
const payment = {
pr: bolt11
};
try {
d.log('Processing payment...');
const result = await currentWallet.lnPay(payment, true);
if (!result) {
d.log('Payment probably successful and sent to the same mint we are using, so there is no preimage. Also there is no tx event emitted from this operation.');
}
else {
d.log('✅ Payment successful with result', result);
}
}
catch (error) {
d.error('❌ Failed to pay invoice:', error);
throw error;
}
return { success: true };
}
// Function to receive a token (e.g. someone shared a cashu token with you)
export async function receiveToken(token) {
dToken.log(`Receiving token: ${token.substring(0, 20)}...`);
const currentWallet = get(wallet);
if (!currentWallet) {
dToken.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
dToken.log('Processing token...');
const result = await currentWallet.receiveToken(token, 'Token received');
dToken.log('✅ Token received successfully', result);
return result;
}
catch (error) {
dToken.error('❌ Failed to receive token:', error);
throw error;
}
}
// Function to generate a token (create ecash to send)
export async function generateToken(amount, recipientMints) {
dToken.log(`Generating token for ${amount} sats`);
const currentWallet = get(wallet);
if (!currentWallet) {
dToken.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
// Check if we have enough total balance
const balance = currentWallet.balance;
if (!balance || balance.amount < amount) {
dToken.error(`Insufficient balance. Need ${amount} sats but have ${balance?.amount || 0} sats.`);
throw new Error(`Insufficient balance. You need ${amount} sats but have ${balance?.amount || 0} sats.`);
}
// // Generate a unique ID for this token
// const nanoId = generateTokenId();
// dToken.log(`Generated token ID: ${nanoId}`);
// const tokenDescription = `Token sent #${nanoId}`;
const tokenDescription = `Token sent`;
// Get available mints with sufficient balance
const eligibleMints = currentWallet.getMintsWithBalance(amount);
dToken.log(`Found ${eligibleMints.length} eligible mints with sufficient balance`);
if (eligibleMints.length === 0) {
dToken.error('No mints with sufficient balance found');
throw new Error('No mints with sufficient balance found');
}
// Determine which mint to use
let mint;
// Check if recipient specified preferred mints
if (recipientMints && recipientMints.length > 0) {
// Find intersection between eligible mints and recipient mints
const compatibleMints = eligibleMints.filter(m => recipientMints.includes(m));
if (compatibleMints.length > 0) {
// We have a compatible mint with the recipient
dToken.log(`Found ${compatibleMints.length} compatible mints with recipient`);
// Try to use main mint if it's in the compatible list
const currentMainMint = get(mainMint);
if (currentMainMint && compatibleMints.includes(currentMainMint)) {
dToken.log(`Using main mint which is compatible: ${currentMainMint}`);
mint = currentMainMint;
}
else {
dToken.log(`Using first compatible mint: ${compatibleMints[0]}`);
mint = compatibleMints[0];
}
}
else {
// No compatible mints, provide helpful error message
const mintBalances = await Promise.all(recipientMints.map(async (m) => {
const bal = currentWallet.mintBalance(m);
return { mint: m, balance: bal || 0 };
}));
const balanceInfo = mintBalances
.map(mb => `${new URL(mb.mint).hostname}: ${mb.balance} sats`)
.join(', ');
dToken.error(`No compatible mints with recipient. Recipient accepts: ${recipientMints.join(', ')}`);
throw new Error(`You don't have enough balance in any of the recipient's preferred mints. ` +
`Recipient accepts: ${recipientMints.map(m => new URL(m).hostname).join(', ')}. ` +
`Your balances: ${balanceInfo}. ` +
`Consider swapping some of your tokens to a compatible mint first.`);
}
}
else {
// No recipient mints specified, use main mint if possible
const currentMainMint = get(mainMint);
if (currentMainMint && eligibleMints.includes(currentMainMint)) {
dToken.log(`Using main mint: ${currentMainMint}`);
mint = currentMainMint;
}
else {
dToken.log(`Main mint not eligible, using alternative: ${eligibleMints[0]}`);
mint = eligibleMints[0];
}
}
// Generate the token using NDK Cashu wallet
const paymentInfo = {
amount: amount,
unit: 'sat',
mints: [mint],
paymentDescription: tokenDescription,
};
dToken.log('Generating payment...', paymentInfo);
const paymentResult = await currentWallet.cashuPay(paymentInfo);
dToken.log('Payment result received');
if (!paymentResult) {
dToken.error('Failed to generate token');
throw new Error('Failed to generate token');
}
// Encode the token using the Cashu format
const token = {
mint: mint,
proofs: paymentResult.proofs,
memo: `${amount} sats from Nostr wallet`
};
dToken.log('Encoding token...');
const encodedToken = getEncodedTokenV4(token);
// // Save the token to the database
// dToken.log('Saving token to database...');
// await saveToken(encodedToken, amount, nanoId, tokenDescription);
// dToken.log('✅ Token generated and saved successfully');
return {
encodedToken,
mint
};
}
catch (error) {
dToken.error('❌ Error generating token:', error);
throw error;
}
}
// Clean up wallet on logout
export function clearWallet() {
d.log('Clearing wallet...');
const currentWallet = get(wallet);
if (currentWallet) {
d.log('Stopping wallet monitoring');
currentWallet.stop(); // Stop listening for events
}
if (txSubscription) {
d.log('Stopping transaction subscription');
txSubscription.stop();
txSubscription = null;
}
wallet.set(undefined);
walletBalance.set(0);
walletTransactions.set([]);
isWalletReady.set(false);
d.log('✅ Wallet cleared');
}
export async function reclaimToken(nanoId) {
dToken.log(`Attempting to reclaim token with ID: ${nanoId}`);
// const currentWallet = get(wallet);
// if (!currentWallet) {
// dToken.error('Wallet not initialized');
// throw new Error('Wallet not initialized');
// }
//
// try {
// // Get the token from the database
// dToken.log('Looking up token in database...');
// const sentToken = await getTokenByNanoId(nanoId);
// if (!sentToken) {
// dToken.error(`Token with ID ${nanoId} not found`);
// throw new Error(`Token with ID ${nanoId} not found`);
// }
//
// if (sentToken.isReclaimed) {
// dToken.log('Token was already reclaimed, deleting from database...');
// // Token was already reclaimed, delete it from the database
// await deleteTokenByNanoId(nanoId);
// throw new Error('This token has already been reclaimed');
// }
//
// try {
// // Attempt to receive the token
// dToken.log('Attempting to receive token...');
// const result = await currentWallet.receiveToken(sentToken.token, 'Token reclaimed');
//
// // If successful, delete the token from the database
// dToken.log('✅ Token reclaimed successfully, deleting from database...');
// await deleteTokenByNanoId(nanoId);
//
// return result;
// } catch (error) {
// // Check if the error indicates the token was already spent
// const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// dToken.error('Error reclaiming token:', errorMessage);
//
// if (
// errorMessage.includes('already spent') ||
// errorMessage.includes('proof not found') ||
// errorMessage.includes('invalid token')
// ) {
// // Token was spent elsewhere, delete it from the database
// dToken.log('Token was spent elsewhere, deleting from database...');
// await deleteTokenByNanoId(nanoId);
// throw new Error('This token has already been spent and cannot be reclaimed');
// }
//
// // For other errors, just pass them through
// throw error;
// }
// } catch (error) {
// dToken.error('❌ Failed to reclaim token:', error);
// throw error;
// }
}
export async function canReclaimTransaction(description) {
return { canReclaim: false };
// if (!description) return { canReclaim: false };
//
// // Check if the description contains our nano ID format
// const match = description.match(/Token sent #([A-Za-z0-9_-]{5})/);
// if (!match) return { canReclaim: false };
//
// const nanoId = match[1];
// dToken.log(`Found nanoId ${nanoId} in transaction description`);
//
// // Check if the token exists in our database
// dToken.log('Checking if token exists in database...');
// const token = await getTokenByNanoId(nanoId);
//
// // If no token found in the database, it's already been handled
// if (!token) {
// dToken.log(`Token with nanoId ${nanoId} not found in database`);
// return { canReclaim: false };
// }
//
// dToken.log(`Token with nanoId ${nanoId} found, can reclaim: ${!token.isReclaimed}`);
// return {
// canReclaim: !token.isReclaimed,
// nanoId
// };
}
// // Function to clean up old tokens in the database
// export async function cleanupOldTokens(olderThanDays = 30) {
// dToken.log(`Cleaning up tokens older than ${olderThanDays} days...`);
// try {
// const cutoffTime = Math.floor(Date.now() / 1000) - olderThanDays * 24 * 60 * 60;
//
// const count = await tokenDb.sentTokens.where('createdAt').below(cutoffTime).delete();
//
// if (count > 0) dToken.log(`✅ Cleaned up ${count} old tokens from the database`);
// else dToken.log('No old tokens to clean up');
// return count;
// } catch (error) {
// dToken.error('❌ Failed to clean up old tokens:', error);
// return 0;
// }
// }
// Function to consolidate tokens
export async function consolidateTokens() {
const currentWallet = get(wallet);
if (!currentWallet) {
d.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
d.log('Starting token consolidation...');
// Get all mints from the wallet
const mints = new Set(currentWallet.state
.getMintsProofs({
validStates: new Set(['available', 'reserved', 'deleted'])
})
.keys());
d.log(`Found ${mints.size} mints to consolidate`);
// Process each mint sequentially
for (const mint of mints) {
if (!mint)
continue;
// Get proofs for this mint
const proofs = currentWallet.state.getProofs({
mint,
includeDeleted: true,
onlyAvailable: false
});
d.log(`Consolidating ${proofs.length} proofs for mint: ${mint}`);
// Use the per-mint consolidation function that supports callbacks
await new Promise((resolve, reject) => {
// Using the NDKCashuWallet's consolidateMintTokens function which has callbacks
consolidateMintTokens(mint, currentWallet, proofs, (walletChange) => {
// Success callback
d.log(`✅ Consolidated mint ${mint}:`, {
stored: walletChange.store?.length || 0,
destroyed: walletChange.destroy?.length || 0
});
resolve();
}, (error) => {
// Error callback
d.error(`❌ Failed to consolidate mint ${mint}:`, error);
reject(new Error(`Failed to consolidate mint: ${error}`));
});
});
}
// Refresh mint info after consolidation
d.log('Refreshing mint info after consolidation...');
await refreshMintInfo();
d.log('✅ Token consolidation complete');
}
catch (error) {
d.error('❌ Failed to consolidate tokens:', error);
throw error;
}
}
// MINT MANAGEMENT FUNCTIONS
export const mintInfo = derived(wallet, ($wallet) => {
if (!$wallet)
return [];
// Get registered mints from wallet.mints (in order)
const registeredMints = $wallet.mints;
// Get all mints with balances from wallet.mintBalances
// Type: Record<MintUrl, number> where MintUrl is a string
const mintBalances = $wallet.mintBalances;
// Create a map to track which mints we've already added
const processedMints = new Set();
// First, add all registered mints in their original order
const allMints = registeredMints.map((url, index) => {
processedMints.add(url);
return {
url,
balance: mintBalances[url] || 0,
isMain: index === 0, // First mint is considered the main mint
isRegistered: true // This is a registered mint
};
});
// Now add any rogue mints (mints with balances that aren't registered)
for (const [url, balance] of Object.entries(mintBalances)) {
// Skip if we've already processed this mint
if (processedMints.has(url))
continue;
// Add the rogue mint
allMints.push({
url,
balance,
isMain: false, // Rogue mints can't be main
isRegistered: false // This is not a registered mint
});
}
return allMints;
});
// Derived store for the main mint
export const mainMint = derived(mintInfo, ($mintInfo) => {
const main = $mintInfo.find((m) => m.isMain);
return main?.url || null;
});
// Function to refresh mint information
export async function refreshMintInfo() {
dMint.log('Refreshing mint information');
const currentWallet = get(wallet);
if (!currentWallet)
return;
// Trigger a wallet update to refresh the derived stores
wallet.update((w) => w);
dMint.log('✅ Mint information refreshed');
}
// Validate a mint URL before adding it
export async function validateMintUrl(url) {
dMint.log(`Validating mint URL: ${url}`);
return validateMint(url.trim());
}
// Add a mint to the wallet
export async function addMint(url) {
dMint.log(`Adding mint: ${url}`);
if (!url.trim()) {
dMint.error('Mint URL cannot be empty');
throw new Error('Mint URL cannot be empty');
}
const currentWallet = get(wallet);
if (!currentWallet) {
dMint.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
// Don't add duplicates
if (currentWallet.mints.includes(url.trim())) {
dMint.warn(`Mint ${url} is already in wallet`);
throw new Error('This mint is already in your wallet');
}
// Validate the mint
dMint.log(`Validating mint ${url}...`);
const validationResult = await validateMint(url.trim());
if (!validationResult.isValid) {
dMint.error(`Invalid mint: ${validationResult.error}`);
throw new Error(`Invalid mint: ${validationResult.error}`);
}
// Add the mint to the wallet
dMint.log(`Adding mint ${url} to wallet...`);
currentWallet.mints.push(url.trim());
// Update the store to trigger derived stores
wallet.update((w) => w);
// Publish the updated wallet and mintlist
dMint.log('Publishing updated wallet and mint list...');
await publishWalletWithMints();
dMint.log(`✅ Mint ${url} added successfully`);
}
catch (error) {
dMint.error(`❌ Error adding mint:`, error);
throw error;
}
}
// Remove a mint from the wallet
export async function removeMint(url) {
dMint.log(`Removing mint: ${url}`);
const currentWallet = get(wallet);
if (!currentWallet) {
dMint.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
// Remove the mint from the wallet
const mintIndex = currentWallet.mints.indexOf(url);
if (mintIndex !== -1) {
dMint.log(`Found mint at index ${mintIndex}, removing...`);
currentWallet.mints.splice(mintIndex, 1);
}
else {
dMint.warn(`Mint ${url} not found in wallet`);
}
// Update the store to trigger derived stores
wallet.update((w) => w);
// Publish the updated wallet and mintlist
dMint.log('Publishing updated wallet and mint list...');
await publishWalletWithMints();
dMint.log(`✅ Mint ${url} removed successfully`);
}
catch (error) {
dMint.error(`❌ Error removing mint:`, error);
throw error;
}
}
// Set a mint as the main mint (move to first position)
export async function setAsMainMint(url) {
dMint.log(`Setting mint as main: ${url}`);
const currentWallet = get(wallet);
if (!currentWallet) {
dMint.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
// Find the mint in the current list
const mintIndex = currentWallet.mints.indexOf(url);
if (mintIndex === -1) {
dMint.error(`Mint ${url} not found in wallet`);
throw new Error('Mint not found in wallet');
}
// Don't do anything if it's already the main mint
if (mintIndex === 0) {
dMint.log(`Mint ${url} is already the main mint, no changes needed`);
return;
}
// Remove the mint from the current position
dMint.log(`Moving mint from position ${mintIndex} to position 0...`);
const mintUrl = currentWallet.mints.splice(mintIndex, 1)[0];
// Add it to the first position
currentWallet.mints.unshift(mintUrl);
// Update the store to trigger derived stores
wallet.update((w) => w);
// Publish the updated wallet and mintlist
dMint.log('Publishing updated wallet and mint list...');
await publishWalletWithMints();
dMint.log(`✅ Mint ${url} set as main successfully`);
}
catch (error) {
dMint.error(`❌ Error setting main mint:`, error);
throw error;
}
}
// Publish wallet and mint list as Nostr events
async function publishWalletWithMints() {
dMint.log('Publishing wallet and mint list...');
const ndk = getNDK();
const currentWallet = get(wallet);
if (!currentWallet) {
dMint.error('Wallet not initialized');
throw new Error('Wallet not initialized');
}
try {
// Publish the updated wallet
dMint.log('Publishing wallet event...');
await currentWallet.publish();
// Also update the mintlist for nutzap reception
dMint.log('Publishing mintlist for nutzap reception...');
const mintlistForNutzapReception = new NDKCashuMintList(ndk);
mintlistForNutzapReception.relays = currentWallet.relaySet?.relayUrls || [];
mintlistForNutzapReception.mints = currentWallet.mints;
mintlistForNutzapReception.p2pk = currentWallet.p2pk;
await mintlistForNutzapReception.publishReplaceable();
dMint.log('✅ Wallet and mint list published successfully');
}
catch (error) {
dMint.error('❌ Failed to publish wallet with updated mints', error);
throw error;
}
}
// /**
// * Creates a deposit token for messaging
// * @returns The encoded token or null if creation fails
// */
// export async function createMessageDeposit(): Promise<string | null> {
// d.log(`Creating message deposit token of ${REQUIRED_DEPOSIT_AMOUNT} sats`);
// const balance = get(walletBalance);
//
// // Check if we have enough funds
// if (balance < REQUIRED_DEPOSIT_AMOUNT) {
// d.error(
// `Insufficient funds. Need at least ${REQUIRED_DEPOSIT_AMOUNT} sats to send a message, but have ${balance} sats.`
// );
// return null;
// }
//
// try {
// // Create a token using the existing generateToken function
// d.log('Generating token for message deposit...');
// const { encodedToken } = await generateToken(REQUIRED_DEPOSIT_AMOUNT);
// d.log('✅ Message deposit token created successfully');
// return encodedToken;
// } catch (error) {
// d.error('❌ Failed to create deposit token:', error);
// return null;
// }
// }