mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
refactor: make GET wallet a bit faster
This commit is contained in:
parent
69fe86890f
commit
aadc3079fa
5 changed files with 214 additions and 28 deletions
|
|
@ -446,7 +446,7 @@ Deno.test('getWallet function is working', async () => {
|
||||||
|
|
||||||
await relay.event(wallet);
|
await relay.event(wallet);
|
||||||
|
|
||||||
const walletEntity = await getWallet(relay, pubkey, signer);
|
const { wallet: walletEntity } = await getWallet(relay, pubkey, signer);
|
||||||
|
|
||||||
assertEquals(walletEntity, {
|
assertEquals(walletEntity, {
|
||||||
balance: 38,
|
balance: 38,
|
||||||
|
|
|
||||||
|
|
@ -244,12 +244,12 @@ async function getWallet(
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
signer: SetRequired<NostrSigner, 'nip44'>,
|
signer: SetRequired<NostrSigner, 'nip44'>,
|
||||||
opts?: { signal?: AbortSignal },
|
opts?: { signal?: AbortSignal },
|
||||||
): Promise<Wallet | undefined> {
|
): Promise<{ wallet: Wallet; error: null } | { wallet: null; error: CustomError }> {
|
||||||
const { data, error } = await validateAndParseWallet(store, signer, pubkey, { signal: opts?.signal });
|
const { data, error } = await validateAndParseWallet(store, signer, pubkey, { signal: opts?.signal });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logi({ level: 'error', ns: 'ditto.cashu.get_wallet', error: errorJson(error) });
|
logi({ level: 'error', ns: 'ditto.cashu.get_wallet', error: errorJson(error) });
|
||||||
return;
|
return { wallet: null, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { p2pk, mints, relays } = data;
|
const { p2pk, mints, relays } = data;
|
||||||
|
|
@ -283,7 +283,7 @@ async function getWallet(
|
||||||
balance,
|
balance,
|
||||||
};
|
};
|
||||||
|
|
||||||
return walletEntity;
|
return { wallet: walletEntity, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Serialize an error into JSON for JSON logging. */
|
/** Serialize an error into JSON for JSON logging. */
|
||||||
|
|
|
||||||
73
packages/ditto/cache/sessionCache.ts
vendored
Normal file
73
packages/ditto/cache/sessionCache.ts
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* A simple in-memory session cache for storing small pieces of data
|
||||||
|
* with an optional TTL.
|
||||||
|
*/
|
||||||
|
export class SessionCache {
|
||||||
|
private cache = new Map<string, { value: any; expires?: number }>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value in the cache
|
||||||
|
* @param key The cache key
|
||||||
|
* @param value The value to store
|
||||||
|
* @param ttlSec Optional TTL in seconds
|
||||||
|
*/
|
||||||
|
set(key: string, value: any, ttlSec?: number): void {
|
||||||
|
const expires = ttlSec ? Date.now() + (ttlSec * 1000) : undefined;
|
||||||
|
this.cache.set(key, { value, expires });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from the cache
|
||||||
|
* @param key The cache key
|
||||||
|
* @returns The cached value or undefined if not found or expired
|
||||||
|
*/
|
||||||
|
get(key: string): any {
|
||||||
|
const item = this.cache.get(key);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.expires && item.expires < Date.now()) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a value from the cache
|
||||||
|
* @param key The cache key
|
||||||
|
*/
|
||||||
|
delete(key: string): void {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all values from the cache
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run cleanup to remove expired items
|
||||||
|
*/
|
||||||
|
cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, item] of this.cache.entries()) {
|
||||||
|
if (item.expires && item.expires < now) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
export const sessionCache = new SessionCache();
|
||||||
|
|
||||||
|
// Run cleanup every minute
|
||||||
|
setInterval(() => {
|
||||||
|
sessionCache.cleanup();
|
||||||
|
}, 60 * 1000);
|
||||||
133
packages/ditto/cache/walletCache.ts
vendored
Normal file
133
packages/ditto/cache/walletCache.ts
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { Wallet } from '@ditto/cashu';
|
||||||
|
import { logi } from '@soapbox/logi';
|
||||||
|
import { errorJson } from '@/utils/log.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple in-memory cache for wallet data
|
||||||
|
* - Keys are pubkeys
|
||||||
|
* - Values are wallet data and timestamp
|
||||||
|
*/
|
||||||
|
interface CachedWallet {
|
||||||
|
wallet: Wallet;
|
||||||
|
timestamp: number;
|
||||||
|
lastQueryTimestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WalletCache {
|
||||||
|
private cache = new Map<string, CachedWallet>();
|
||||||
|
private ttlMs: number;
|
||||||
|
private queryTtlMs: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ttlSec Cache TTL in seconds
|
||||||
|
* @param queryTtlSec How long we should wait between full queries in seconds
|
||||||
|
*/
|
||||||
|
constructor(ttlSec = 60, queryTtlSec = 5) {
|
||||||
|
this.ttlMs = ttlSec * 1000;
|
||||||
|
this.queryTtlMs = queryTtlSec * 1000;
|
||||||
|
|
||||||
|
// Periodic cleanup
|
||||||
|
setInterval(() => this.cleanup(), 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet from cache
|
||||||
|
* @param pubkey User's pubkey
|
||||||
|
* @returns The cached wallet if present and valid, null otherwise
|
||||||
|
*/
|
||||||
|
get(pubkey: string): { wallet: Wallet; shouldRefresh: boolean } | null {
|
||||||
|
const entry = this.cache.get(pubkey);
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const age = now - entry.timestamp;
|
||||||
|
|
||||||
|
// If cache entry is too old, consider it invalid
|
||||||
|
if (age > this.ttlMs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should refresh the data in the background
|
||||||
|
// This is determined by how long since the last full query
|
||||||
|
const queryAge = now - entry.lastQueryTimestamp;
|
||||||
|
const shouldRefresh = queryAge > this.queryTtlMs;
|
||||||
|
|
||||||
|
return { wallet: entry.wallet, shouldRefresh };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store wallet in cache
|
||||||
|
* @param pubkey User's pubkey
|
||||||
|
* @param wallet Wallet data
|
||||||
|
* @param isQueryResult Whether this is from a full query or just a balance update
|
||||||
|
*/
|
||||||
|
set(pubkey: string, wallet: Wallet, isQueryResult = true): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const existing = this.cache.get(pubkey);
|
||||||
|
|
||||||
|
this.cache.set(pubkey, {
|
||||||
|
wallet,
|
||||||
|
timestamp: now,
|
||||||
|
// If this is just a balance update, preserve the lastQueryTimestamp
|
||||||
|
lastQueryTimestamp: isQueryResult ? now : (existing?.lastQueryTimestamp || now),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update balance for a wallet without doing a full refresh
|
||||||
|
* @param pubkey User's pubkey
|
||||||
|
* @param deltaAmount Amount to add to balance (negative to subtract)
|
||||||
|
* @returns true if updated, false if wallet not in cache
|
||||||
|
*/
|
||||||
|
updateBalance(pubkey: string, deltaAmount: number): boolean {
|
||||||
|
const entry = this.cache.get(pubkey);
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWallet = {
|
||||||
|
...entry.wallet,
|
||||||
|
balance: entry.wallet.balance + deltaAmount,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.set(pubkey, newWallet, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove expired entries from cache
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
for (const [pubkey, entry] of this.cache.entries()) {
|
||||||
|
if (now - entry.timestamp > this.ttlMs) {
|
||||||
|
this.cache.delete(pubkey);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
logi({
|
||||||
|
level: 'debug',
|
||||||
|
ns: 'ditto.cache.wallet',
|
||||||
|
message: `Cleaned up ${deletedCount} expired wallet cache entries`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
getStats(): { size: number } {
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const walletCache = new WalletCache();
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import { CashuMint, CashuWallet, MintQuoteState, Proof } from '@cashu/cashu-ts';
|
import { CashuMint, CashuWallet, MintQuoteState, Proof } from '@cashu/cashu-ts';
|
||||||
import {
|
import { getWallet, organizeProofs, proofSchema, renderTransaction, tokenEventSchema, type Wallet } from '@ditto/cashu';
|
||||||
getWallet,
|
|
||||||
organizeProofs,
|
|
||||||
proofSchema,
|
|
||||||
renderTransaction,
|
|
||||||
tokenEventSchema,
|
|
||||||
validateAndParseWallet,
|
|
||||||
type Wallet,
|
|
||||||
} from '@ditto/cashu';
|
|
||||||
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
import { paginated, paginationSchema } from '@ditto/mastoapi/pagination';
|
import { paginated, paginationSchema } from '@ditto/mastoapi/pagination';
|
||||||
import { DittoRoute } from '@ditto/mastoapi/router';
|
import { DittoRoute } from '@ditto/mastoapi/router';
|
||||||
|
|
@ -28,13 +20,6 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
|
|
||||||
const route = new DittoRoute();
|
const route = new DittoRoute();
|
||||||
|
|
||||||
interface Nutzap {
|
|
||||||
amount: number;
|
|
||||||
event_id?: string;
|
|
||||||
mint: string; // mint the nutzap was created
|
|
||||||
recipient_pubkey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createMintQuoteSchema = z.object({
|
const createMintQuoteSchema = z.object({
|
||||||
mint: z.string().url(),
|
mint: z.string().url(),
|
||||||
amount: z.number().int(),
|
amount: z.number().int(),
|
||||||
|
|
@ -258,18 +243,13 @@ route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, as
|
||||||
|
|
||||||
const pubkey = await user.signer.getPublicKey();
|
const pubkey = await user.signer.getPublicKey();
|
||||||
|
|
||||||
const { error } = await validateAndParseWallet(relay, user.signer, pubkey, { signal });
|
const { wallet, error } = await getWallet(relay, pubkey, user.signer, { signal });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return c.json({ error: error.message }, 404);
|
return c.json({ error: error.message }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const walletEntity = await getWallet(relay, pubkey, user.signer);
|
return c.json(wallet, 200);
|
||||||
|
|
||||||
if (!walletEntity) {
|
|
||||||
return c.json({ 'error': 'Wallet not found' }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json(walletEntity, 200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Gets a history of transactions. */
|
/** Gets a history of transactions. */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue