mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
Merge pull request #125 from Routstr/fix/provider-mint-awareness
fix(daemon): populate the provider mint cache during bootstrap/refresh (finding #4)
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,94 @@ 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();
|
||||
});
|
||||
|
||||
it("does not await an outstanding mint discovery pass or duplicate it", async () => {
|
||||
let resolveDiscovery!: () => void;
|
||||
const pending = new Promise<void>((resolve) => { resolveDiscovery = resolve; });
|
||||
let calls = 0;
|
||||
const modelManager = {
|
||||
bootstrapProviders: async () => ["https://a.example/"],
|
||||
syncReviewedProvidersFromNostr: async () => [],
|
||||
} as never;
|
||||
const discovery = {
|
||||
discoverMints: () => { calls++; return pending; },
|
||||
} as never;
|
||||
const service = createModelService(modelManager, {} as never, makeStore().store, discovery);
|
||||
try {
|
||||
await Promise.race([
|
||||
service.ensureProvidersBootstrapped(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("bootstrap blocked on mint discovery")), 100)),
|
||||
]);
|
||||
await service.ensureProvidersBootstrapped();
|
||||
expect(calls).toBe(1);
|
||||
} finally {
|
||||
resolveDiscovery();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
findModelForId,
|
||||
ModelManager,
|
||||
ProviderManager,
|
||||
type MintDiscovery,
|
||||
type Model,
|
||||
type SdkStore,
|
||||
} from "@routstr/sdk";
|
||||
@@ -28,8 +29,22 @@ export function createModelService(
|
||||
modelManager: ModelManager,
|
||||
providerManager: ProviderManager,
|
||||
store: SdkStore,
|
||||
mintDiscovery?: MintDiscovery,
|
||||
) {
|
||||
let providerBootstrapPromise: Promise<void> | null = null;
|
||||
let mintDiscoveryPromise: Promise<void> | null = null;
|
||||
|
||||
const scheduleMintDiscovery = (providers: string[]): void => {
|
||||
if (!mintDiscovery || mintDiscoveryPromise) return;
|
||||
const discovery = mintDiscovery;
|
||||
mintDiscoveryPromise = Promise.resolve()
|
||||
.then(async () => {
|
||||
await discovery.discoverMints(providers);
|
||||
logger.log(`Discovered mints for ${providers.length} provider(s)`);
|
||||
})
|
||||
.catch((error) => logger.error("Mint discovery failed:", error))
|
||||
.finally(() => { mintDiscoveryPromise = null; });
|
||||
};
|
||||
|
||||
const normalizeBaseUrl = (url: string): string =>
|
||||
url.endsWith("/") ? url : `${url}/`;
|
||||
@@ -132,6 +147,13 @@ 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.
|
||||
scheduleMintDiscovery(providers);
|
||||
})().catch((error) => {
|
||||
providerBootstrapPromise = null;
|
||||
logger.error("Provider bootstrap failed:", error);
|
||||
@@ -278,6 +300,10 @@ 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).
|
||||
scheduleMintDiscovery(providers);
|
||||
|
||||
// Sync review events from Nostr (kind 38425) and apply disabled status
|
||||
const reviewedDisabled = await modelManager.syncReviewedProvidersFromNostr(
|
||||
providers,
|
||||
|
||||
Reference in New Issue
Block a user