Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion hooks/useSdkCachedBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export function useSdkCachedBalance(): number {
let unsubscribe: (() => void) | null = null;

const computeBalance = () => {
const tokens = storeRef.current.store.getState().cachedTokens;
// Use apiKeys for cached balance calculation (renamed from cachedTokens)
const tokens = storeRef.current.store.getState().apiKeys;
const total = tokens.reduce((sum, t) => sum + (t.balance || 0), 0);
setCachedBalance(total);
};
Expand Down
5 changes: 0 additions & 5 deletions hooks/useSdkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ const createPendingDeps = (): {
getAllProvidersModels: () => ({}),
};
const pendingStorage: StorageAdapter = {
getToken: pendingHandler,
setToken: pendingHandler,
removeToken: pendingHandler,
updateTokenBalance: pendingHandler,
getCachedTokenDistribution: () => [],
saveProviderInfo: pendingHandler,
getProviderInfo: () => null,
getApiKey: () => null,
Expand Down
24 changes: 23 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ const nextConfig: NextConfig = {
},
// Add HMR configuration to prevent ping errors
webpack: (config, { dev, isServer }) => {
// Prevent native Node.js modules from being bundled for the client
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
crypto: false,
};
// Ignore native modules that shouldn't be bundled for client
config.module.rules.push({
test: /node_modules\/better-sqlite3\/.*/,
use: { loader: "null-loader" },
});
config.module.rules.push({
test: /node_modules\/bindings\/.*/,
use: { loader: "null-loader" },
});
config.module.rules.push({
test: /node_modules\/file-uri-to-path\/.*/,
use: { loader: "null-loader" },
});
}
if (dev && !isServer) {
config.watchOptions = {
poll: 1000,
Expand All @@ -64,7 +86,7 @@ const nextConfig: NextConfig = {
}
return config;
},
serverExternalPackages: [],
serverExternalPackages: ["better-sqlite3"],
// Silence Next 16 Turbopack + webpack plugin warning (next-pwa injects webpack config)
// See: https://nextjs.org/docs/app/api-reference/next-config-js/turbopack
turbopack: {},
Expand Down
50 changes: 49 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"rxjs": "^7.8.1",
"server-only": "^0.0.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -105,6 +106,7 @@
"eslint-config-next": "16.0.1",
"eslint-config-prettier": "^10.1.8",
"msw": "^2.11.3",
"null-loader": "^4.0.1",
"playwright": "^1.56.1",
"prettier": "^3.7.4",
"tailwindcss": "^4",
Expand Down
16 changes: 9 additions & 7 deletions scripts/check-balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { getDecodedToken } from "@cashu/cashu-ts";

interface BalanceSummary {
walletBalance: Record<string, number>;
cachedTokens: Array<{ baseUrl: string; balance: number }>;
apiKeys: Array<{ baseUrl: string; balance: number }>;
childKeys: Array<{ parentBaseUrl: string; balance: number }>;
cachedReceiveTokens: Array<{ baseUrl: string; amount: number }>;
}

async function runWalletCommand(args: string[]): Promise<string> {
Expand Down Expand Up @@ -153,14 +153,16 @@ async function main(): Promise<void> {
}
console.log(` Total: ${totalWallet} sats\n`);

console.log("=== Cached Tokens ===");
const cachedTokens = store.getState().cachedTokens;
const totalCached = cachedTokens.reduce(
(sum, t) => sum + (t.balance || 0),
console.log("=== Cached Receive Tokens ===");
const cachedReceiveTokens = store.getState().cachedReceiveTokens;
const totalCached = cachedReceiveTokens.reduce(
(sum, t) => sum + (t.amount || 0),
0
);
for (const token of cachedTokens) {
console.log(` ${token.baseUrl}: ${token.balance || 0} sats`);
for (const token of cachedReceiveTokens) {
// cachedReceiveTokens don't have baseUrl, just show token preview
const tokenPreview = token.token.substring(0, 20) + "...";
console.log(` ${tokenPreview}: ${token.amount || 0} sats (${token.unit})`);
}
console.log(` Total: ${totalCached} sats\n`);

Expand Down
16 changes: 8 additions & 8 deletions scripts/refund-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,27 +81,27 @@ async function main(): Promise<void> {
const storageAdapter = createStorageAdapterFromStore(store);
const providerRegistry = createProviderRegistryFromStore(store);

const pendingDistribution = storageAdapter.getCachedTokenDistribution();
// Get cached receive tokens (tokens that failed to receive)
const cachedReceiveTokens = storageAdapter.getCachedReceiveTokens();
const apiKeysStored = storageAdapter.getApiKeyDistribution();

if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
if (cachedReceiveTokens.length === 0 && apiKeysStored.length === 0) {
console.log("No pending tokens to refund");
return;
}

console.log(`Found ${pendingDistribution.length} pending tokens:`);
for (const pending of pendingDistribution) {
console.log(` - ${pending.baseUrl}: ${pending.amount} sats`);
console.log(`Found ${cachedReceiveTokens.length} cached receive tokens:`);
for (const pending of cachedReceiveTokens) {
console.log(` - ${pending.token.substring(0, 20)}...: ${pending.amount} sats (${pending.unit})`);
}

console.log(`Found ${apiKeysStored.length} apikeys:`);
for (const apikey of apiKeysStored) {
console.log(` - ${apikey.baseUrl}: ${apikey.amount} sats`);
}

const refundBaseUrls = pendingDistribution
.map((p) => p.baseUrl)
.concat(apiKeysStored.map((p) => p.baseUrl));
// Use API keys for refund base URLs
const refundBaseUrls = apiKeysStored.map((p) => p.baseUrl);

let mintUnits: Record<string, "sat" | "msat"> = {};

Expand Down
19 changes: 10 additions & 9 deletions scripts/set-client-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ Examples:
const currentClientIds = store.getState().clientIds;

if (mode === "set") {
// TypeScript narrows the type after the early exit check above
const newEntry: ClientIdEntry = {
clientId,
name,
apiKey,
clientId: clientId as string,
name: name as string,
apiKey: apiKey as string,
createdAt: Date.now(),
lastUsed: null,
};
Expand All @@ -129,19 +130,19 @@ Examples:
let updated: ClientIdEntry[];

if (existingIndex !== -1) {
// Update existing
// Update existing - TypeScript knows name and apiKey are not null here due to earlier check
updated = currentClientIds.map((e, i) =>
i === existingIndex
? { ...e, name, apiKey, lastUsed: Date.now() }
? { ...e, name: name as string, apiKey: apiKey as string, lastUsed: Date.now() }
: e
);
console.log(`ClientId "${clientId}" has been updated.\n`);
} else {
// Add new
// Add new - TypeScript knows name and apiKey are not null here due to earlier check
const newEntry: ClientIdEntry = {
clientId: clientId!,
name: name!,
apiKey: apiKey!,
clientId: clientId as string,
name: name as string,
apiKey: apiKey as string,
createdAt: Date.now(),
lastUsed: null,
};
Expand Down
2 changes: 1 addition & 1 deletion sdk/client/RoutstrClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface FetchOptions {
* RoutstrClient is the main SDK entry point
*/
export type AlertLevel = "max" | "min";
export type RoutstrClientMode = "xcashu" | "apikeys";
export type RoutstrClientMode = "xcashu" | "apikeys" | "lazyrefund";
export type DebugLevel = "DEBUG" | "WARN" | "ERROR";

const TOPUP_MARGIN = 1.2;
Expand Down
4 changes: 2 additions & 2 deletions sdk/routeRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export interface RouteRequestOptions {
modelManager?: ModelManager;
/** Optional: set RoutstrClient debug level */
debugLevel?: DebugLevel;
/** Optional: client mode (xcashu or apikeys) */
mode?: "xcashu" | "apikeys";
/** Optional: client mode (xcashu, apikeys, or lazyrefund) */
mode?: "xcashu" | "apikeys" | "lazyrefund";
}

export interface RouteRequestToNodeResponseOptions extends RouteRequestOptions {
Expand Down
54 changes: 39 additions & 15 deletions sdk/storage/drivers/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ const isBun = (): boolean => {
return typeof process.versions.bun !== "undefined";
};

const createDatabase = (dbPath: string): BetterSqlite3Database => {
// Lazy-load better-sqlite3 to avoid bundling it for client-side code
let cachedDbModule: any = null;

const loadDatabase = async (dbPath: string): Promise<BetterSqlite3Database> => {
if (isBun()) {
throw new Error(
"SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
);
}

let Database: any = null;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Database = require("better-sqlite3");
if (!cachedDbModule) {
cachedDbModule = (await import("better-sqlite3")).default;
}
return new cachedDbModule(dbPath);
} catch (error) {
throw new Error(
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
);
}
return new Database(dbPath);
};

export const createSqliteDriver = (
Expand All @@ -43,21 +46,38 @@ export const createSqliteDriver = (
const dbPath = options.dbPath || "routstr.sqlite";
const tableName = options.tableName || "sdk_storage";

const db = createDatabase(dbPath);
db.exec(
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
);
let db: BetterSqlite3Database;
let selectStmt: any;
let upsertStmt: any;
let deleteStmt: any;

const initDb = async () => {
if (!db) {
db = await loadDatabase(dbPath);
db.exec(
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
);

const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
const upsertStmt = db.prepare(
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
upsertStmt = db.prepare(
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
);
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
);
deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
}
};

// Initialize asynchronously on first use
const ensureInit = async () => {
if (!db) {
await initDb();
}
};

return {
async getItem<T>(key: string, defaultValue: T): Promise<T> {
try {
await ensureInit();
const row = selectStmt.get(key);
if (!row || typeof row.value !== "string") return defaultValue;
try {
Expand All @@ -75,13 +95,15 @@ export const createSqliteDriver = (
},
async setItem<T>(key: string, value: T): Promise<void> {
try {
await ensureInit();
upsertStmt.run(key, JSON.stringify(value));
} catch (error) {
console.error(`SQLite setItem failed for key "${key}":`, error);
}
},
async removeItem(key: string): Promise<void> {
try {
await ensureInit();
deleteStmt.run(key);
} catch (error) {
console.error(`SQLite removeItem failed for key "${key}":`, error);
Expand All @@ -96,7 +118,9 @@ export async function createBunSqliteDriver(
dbPath: string
): Promise<StorageDriver> {
// @ts-ignore - bun:sqlite is only available at runtime in Bun environments
const SQLite = (await import("bun:sqlite")).default;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const SQLite = (await import(/* webpackIgnore: true */ "bun:sqlite")).default;
const db = new SQLite(dbPath);

db.run(`
Expand Down
Loading