diff --git a/packages/wallet/wallet-toolbox/CHANGELOG.md b/packages/wallet/wallet-toolbox/CHANGELOG.md index 95d40d9ce..ccf749db4 100644 --- a/packages/wallet/wallet-toolbox/CHANGELOG.md +++ b/packages/wallet/wallet-toolbox/CHANGELOG.md @@ -6,6 +6,11 @@ attention to changes that materially alter behavior or extend functionality. ## wallet-toolbox (unreleased) +- Fix `WalletStorageManager.getStoreEndpointURL` / `getStores().endpointURL` to + duck-type provider `endpointUrl` instead of matching + `constructor.name === 'StorageClient'`. Production minifiers rename classes, + so the name check left remote stores with `endpointURL: undefined` while + sync still worked; clients that select a backup by URL (make primary) failed. - Point Wallet Toolbox contributors and AI agents to the canonical stack-level contribution and quality policy; keep client and mobile candidate versions in release lockstep without changing runtime behavior. diff --git a/packages/wallet/wallet-toolbox/src/storage/WalletStorageManager.ts b/packages/wallet/wallet-toolbox/src/storage/WalletStorageManager.ts index 008812c3e..f657f9252 100644 --- a/packages/wallet/wallet-toolbox/src/storage/WalletStorageManager.ts +++ b/packages/wallet/wallet-toolbox/src/storage/WalletStorageManager.ts @@ -25,7 +25,6 @@ import { TableUser } from '../storage/schema/tables' import { StorageProvider } from './StorageProvider' -import { StorageClient } from './remoting/StorageClient' class ManagedStorage { isAvailable: boolean @@ -955,8 +954,20 @@ export class WalletStorageManager implements sdk.WalletStorage { return log } + /** + * Return the remote HTTP(S) endpoint for a managed store, if any. + * + * Duck-types `endpointUrl` on the provider (as set by `StorageClientBase`). + * Do **not** key this off `constructor.name === 'StorageClient'`: production + * minifiers (Vite/esbuild/webpack) rename classes, so that check fails and + * every remote store reports `endpointURL: undefined` even though the URL is + * present. Consumers that match backups by URL (e.g. making a remote store + * primary) then fail while sync still works, because sync walks `_backups` + * without needing `endpointURL`. + */ getStoreEndpointURL(store: ManagedStorage): string | undefined { - if (store.storage.constructor.name === 'StorageClient') return (store.storage as StorageClient).endpointUrl + const url = (store.storage as { endpointUrl?: unknown }).endpointUrl + if (typeof url === 'string' && url.length > 0) return url return undefined } diff --git a/packages/wallet/wallet-toolbox/test/storage/getStoreEndpointURL.test.ts b/packages/wallet/wallet-toolbox/test/storage/getStoreEndpointURL.test.ts new file mode 100644 index 000000000..3897c23e6 --- /dev/null +++ b/packages/wallet/wallet-toolbox/test/storage/getStoreEndpointURL.test.ts @@ -0,0 +1,83 @@ +/** + * getStoreEndpointURL / getStores().endpointURL must survive production + * minification. Class names are rewritten, so constructor.name is not stable. + */ +import { WalletStorageManager } from '../../src/storage/WalletStorageManager' + +function managedStore(storage: object, storageIdentityKey = 'store-key') { + return { + isAvailable: true, + isStorageProvider: false, + settings: { + storageIdentityKey, + storageName: 'test_store', + chain: 'test', + dbName: 'test', + storageSchemaVersion: 1, + maxOutputScript: 0 + }, + user: { + userId: 1, + identityKey: '02' + '11'.repeat(32), + activeStorage: storageIdentityKey + }, + storage + } as any +} + +describe('WalletStorageManager.getStoreEndpointURL', () => { + it('returns endpointUrl for a StorageClient-shaped provider', () => { + class StorageClient { + endpointUrl = 'https://store-us-1.bsvb.tech' + } + const manager = new WalletStorageManager('identity') + const store = managedStore(new StorageClient()) + expect(manager.getStoreEndpointURL(store)).toBe('https://store-us-1.bsvb.tech') + }) + + it('returns endpointUrl when constructor.name is minified (not StorageClient)', () => { + // Vite/esbuild minify renames classes to single-letter identifiers. + class a { + endpointUrl = 'https://store-us-1.bsvb.tech/' + } + expect(new a().constructor.name).not.toBe('StorageClient') + + const manager = new WalletStorageManager('identity') + const store = managedStore(new a()) + expect(manager.getStoreEndpointURL(store)).toBe('https://store-us-1.bsvb.tech/') + }) + + it('returns undefined for local providers without endpointUrl', () => { + class StorageKnex {} + const manager = new WalletStorageManager('identity') + expect(manager.getStoreEndpointURL(managedStore(new StorageKnex()))).toBeUndefined() + }) + + it('returns undefined for empty endpointUrl strings', () => { + const manager = new WalletStorageManager('identity') + expect(manager.getStoreEndpointURL(managedStore({ endpointUrl: '' }))).toBeUndefined() + }) + + it('exposes endpointURL via getStores even when class name is mangled', () => { + class a { + endpointUrl = 'https://store-us-1.bsvb.tech' + } + class LocalStore {} + + const manager = new WalletStorageManager('identity') + // Bypass makeAvailable: populate partitions the same way makeAvailable would. + ;(manager as any)._isAvailable = true + ;(manager as any)._active = managedStore(new LocalStore(), 'local-key') + ;(manager as any)._backups = [managedStore(new a(), 'remote-key')] + ;(manager as any)._conflictingActives = [] + + const stores = manager.getStores() + expect(stores).toHaveLength(2) + + const local = stores.find(s => s.storageIdentityKey === 'local-key') + const remote = stores.find(s => s.storageIdentityKey === 'remote-key') + expect(local?.endpointURL).toBeUndefined() + expect(remote?.endpointURL).toBe('https://store-us-1.bsvb.tech') + expect(remote?.isBackup).toBe(true) + }) +})