mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
fix(daemon): populate the provider mint cache during bootstrap/refresh
Finding #4, part 1: MintDiscovery.discoverMints had zero call sites in the daemon, so the SDK's mint cache was always empty, getCachedMints() returned {} and the SDK spent the wallet's largest mint regardless of what a node accepts. Construct a shared MintDiscovery (own 21-minute TTL) and run it for every discovered provider during ensureProvidersBootstrapped and refreshProvidersAndModels. Discovery is best-effort: a provider that fails must not abort model bootstrap or block request handling. Verified live with a scratch daemon (separate HOME/port): it persisted mints_from_all_providers, with routstr.cypherpunk.today advertising cashu.cz/21mint.me/coinos.io/cubabitcoin/mountainlake/macadamia and NO minibits, while nostrmodels/otrta advertise minibits. Tests: bootstrap calls discoverMints for every provider; a throwing discovery does not fail bootstrap. Suite: 441 passing.
This commit is contained in:
+7
-1
@@ -2,6 +2,7 @@ import { createServer } from "http";
|
||||
import { existsSync, unlinkSync } from "fs";
|
||||
import {
|
||||
ProviderManager,
|
||||
MintDiscovery,
|
||||
createStorageAdapterFromStore,
|
||||
createSdkStore,
|
||||
} from "@routstr/sdk";
|
||||
@@ -132,8 +133,13 @@ export async function runDaemon(argv: string[] = process.argv): Promise<void> {
|
||||
});
|
||||
// Create shared ProviderManager for consistent failure tracking across all requests
|
||||
const providerManager = new ProviderManager(discoveryAdapter, store, daemonSdkLogger);
|
||||
// Mint discovery owns the mint cache the wallet uses to refuse a mint a
|
||||
// provider does not accept. Its own 21-minute TTL gates network refreshes.
|
||||
const mintDiscovery = new MintDiscovery(discoveryAdapter, {
|
||||
logger: daemonSdkLogger,
|
||||
});
|
||||
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } =
|
||||
createModelService(modelManager, providerManager, store);
|
||||
createModelService(modelManager, providerManager, store, mintDiscovery);
|
||||
|
||||
// The daemon may be launched directly (or by an older/global CLI), so do
|
||||
// not rely on the parent command having stopped the external wallet first.
|
||||
|
||||
@@ -289,3 +289,69 @@ describe("createModelService.getRoutstr21Models", () => {
|
||||
expect(models).toEqual([{ id: "no-such-model", name: "no-such-model" }]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Finding #4: the SDK refuses to spend a mint a provider does not advertise,
|
||||
* so the daemon must actually populate the mint cache during bootstrap (and
|
||||
* refresh). `discoverMints` was previously never called, leaving
|
||||
* `getCachedMints()` empty and every provider "accepting all mints".
|
||||
*/
|
||||
describe("createModelService mint discovery wiring", () => {
|
||||
function makeStore() {
|
||||
const state: Record<string, unknown> = {
|
||||
baseUrlsList: [],
|
||||
setBaseUrlsList: () => {},
|
||||
disabledProviders: [],
|
||||
setDisabledProviders: () => {},
|
||||
manuallyDisabledProviders: [],
|
||||
manuallyEnabledProviders: [],
|
||||
};
|
||||
return { store: { getState: () => state } as unknown as SdkStore };
|
||||
}
|
||||
|
||||
it("discovers mints for every bootstrapped provider", async () => {
|
||||
const providers = ["https://a.example/", "https://b.example/"];
|
||||
const modelManager = {
|
||||
bootstrapProviders: async () => providers,
|
||||
syncReviewedProvidersFromNostr: async () => [],
|
||||
} as never;
|
||||
const discoverCalls: string[][] = [];
|
||||
const mintDiscovery = {
|
||||
discoverMints: async (urls: string[]) => {
|
||||
discoverCalls.push(urls);
|
||||
return { mintsFromProviders: {}, infoFromProviders: {} };
|
||||
},
|
||||
} as never;
|
||||
|
||||
const service = createModelService(
|
||||
modelManager as never,
|
||||
{} as never,
|
||||
makeStore().store,
|
||||
mintDiscovery as never,
|
||||
);
|
||||
await service.ensureProvidersBootstrapped();
|
||||
|
||||
expect(discoverCalls).toEqual([providers]);
|
||||
});
|
||||
|
||||
it("does not fail bootstrap when mint discovery throws", async () => {
|
||||
const modelManager = {
|
||||
bootstrapProviders: async () => ["https://a.example/"],
|
||||
syncReviewedProvidersFromNostr: async () => [],
|
||||
} as never;
|
||||
const mintDiscovery = {
|
||||
discoverMints: async () => {
|
||||
throw new Error("mint discovery boom");
|
||||
},
|
||||
} as never;
|
||||
|
||||
const service = createModelService(
|
||||
modelManager as never,
|
||||
{} as never,
|
||||
makeStore().store,
|
||||
mintDiscovery as never,
|
||||
);
|
||||
|
||||
await expect(service.ensureProvidersBootstrapped()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
findModelForId,
|
||||
ModelManager,
|
||||
ProviderManager,
|
||||
type MintDiscovery,
|
||||
type Model,
|
||||
type SdkStore,
|
||||
} from "@routstr/sdk";
|
||||
@@ -28,6 +29,7 @@ export function createModelService(
|
||||
modelManager: ModelManager,
|
||||
providerManager: ProviderManager,
|
||||
store: SdkStore,
|
||||
mintDiscovery?: MintDiscovery,
|
||||
) {
|
||||
let providerBootstrapPromise: Promise<void> | null = null;
|
||||
|
||||
@@ -132,6 +134,24 @@ export function createModelService(
|
||||
}
|
||||
|
||||
logger.log("Provider bootstrap complete.");
|
||||
|
||||
// Populate the mint cache for every enabled provider. The SDK refuses
|
||||
// to spend a mint a provider does not advertise, so without this the
|
||||
// routing layer spends the wallet's largest mint and gets rejected
|
||||
// (finding #4). MintDiscovery owns its own 21-minute TTL, so repeated
|
||||
// calls within that window are cache hits and never refetch.
|
||||
if (mintDiscovery) {
|
||||
try {
|
||||
await mintDiscovery.discoverMints(providers);
|
||||
logger.log(
|
||||
`Discovered mints for ${providers.length} provider(s)`,
|
||||
);
|
||||
} catch (error) {
|
||||
// Mint discovery is best-effort: a provider that fails here must
|
||||
// not abort model bootstrap or block request handling.
|
||||
logger.error("Mint discovery failed:", error);
|
||||
}
|
||||
}
|
||||
})().catch((error) => {
|
||||
providerBootstrapPromise = null;
|
||||
logger.error("Provider bootstrap failed:", error);
|
||||
@@ -278,6 +298,16 @@ export function createModelService(
|
||||
const models = await modelManager.fetchModels(providers, true);
|
||||
console.log(`Fetched ${models.length} models from ${providers.length} providers`);
|
||||
|
||||
// Refresh each provider's advertised mint list alongside its models so
|
||||
// routing always has current mint data (TTL-gated by MintDiscovery).
|
||||
if (mintDiscovery) {
|
||||
try {
|
||||
await mintDiscovery.discoverMints(providers);
|
||||
} catch (error) {
|
||||
console.error("Mint discovery failed during refresh:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync review events from Nostr (kind 38425) and apply disabled status
|
||||
const reviewedDisabled = await modelManager.syncReviewedProvidersFromNostr(
|
||||
providers,
|
||||
|
||||
Reference in New Issue
Block a user