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
5 changes: 5 additions & 0 deletions packages/wallet/wallet-toolbox/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
TableUser
} from '../storage/schema/tables'
import { StorageProvider } from './StorageProvider'
import { StorageClient } from './remoting/StorageClient'

class ManagedStorage {
isAvailable: boolean
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading