Skip to content

Commit cfbe340

Browse files
authored
Merge pull request #100 from ethereum-optimism/harry/hosted_wallet_registry
feat: introduce `HostedWalletProviderRegistry`
2 parents f25189e + 053147e commit cfbe340

File tree

7 files changed

+178
-46
lines changed

7 files changed

+178
-46
lines changed

packages/demo/backend/src/config/verbs.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ export function createVerbsConfig(): VerbsConfig {
1212
hostedWalletConfig: {
1313
provider: {
1414
type: 'privy',
15-
privyClient: new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET),
15+
config: {
16+
privyClient: new PrivyClient(
17+
env.PRIVY_APP_ID,
18+
env.PRIVY_APP_SECRET,
19+
),
20+
},
1621
},
1722
},
1823
smartWalletConfig: {

packages/sdk/src/types/verbs.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { PrivyClient } from '@privy-io/server-auth'
2-
31
import type { ChainConfig } from '@/types/chain.js'
2+
import type { HostedProviderUnion } from '@/wallet/providers/hostedProvider.types.js'
43

54
import type { LendConfig } from './lend.js'
65

@@ -34,7 +33,7 @@ export type WalletConfig = {
3433
*/
3534
export interface HostedWalletConfig {
3635
/** Wallet provider for account creation, management, and signing */
37-
provider: HostedWalletProviderConfig
36+
provider: HostedProviderUnion
3837
}
3938

4039
/**
@@ -59,17 +58,3 @@ export type SmartWalletProvider = DefaultSmartWalletProvider
5958
export interface DefaultSmartWalletProvider {
6059
type: 'default'
6160
}
62-
63-
/**
64-
* Hosted wallet provider configurations
65-
* @description Union type supporting multiple hosted wallet providers
66-
*/
67-
export type HostedWalletProviderConfig = PrivyHostedWalletProviderConfig
68-
69-
/** Privy hosted wallet provider configuration */
70-
export interface PrivyHostedWalletProviderConfig {
71-
/** Hosted wallet provider type */
72-
type: 'privy'
73-
/** Privy client instance */
74-
privyClient: PrivyClient
75-
}

packages/sdk/src/verbs.test.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ describe('Verbs SDK - System Tests', () => {
2727
hostedWalletConfig: {
2828
provider: {
2929
type: 'privy',
30-
privyClient: createMockPrivyClient(
31-
'test-app-id',
32-
'test-app-secret',
33-
),
30+
config: {
31+
privyClient: createMockPrivyClient(
32+
'test-app-id',
33+
'test-app-secret',
34+
),
35+
},
3436
},
3537
},
3638
smartWalletConfig: {
@@ -103,10 +105,12 @@ describe('Verbs SDK - System Tests', () => {
103105
hostedWalletConfig: {
104106
provider: {
105107
type: 'privy',
106-
privyClient: createMockPrivyClient(
107-
'test-app-id',
108-
'test-app-secret',
109-
),
108+
config: {
109+
privyClient: createMockPrivyClient(
110+
'test-app-id',
111+
'test-app-secret',
112+
),
113+
},
110114
},
111115
},
112116
smartWalletConfig: {
@@ -150,10 +154,12 @@ describe('Verbs SDK - System Tests', () => {
150154
hostedWalletConfig: {
151155
provider: {
152156
type: 'privy',
153-
privyClient: createMockPrivyClient(
154-
'test-app-id',
155-
'test-app-secret',
156-
),
157+
config: {
158+
privyClient: createMockPrivyClient(
159+
'test-app-id',
160+
'test-app-secret',
161+
),
162+
},
157163
},
158164
},
159165
smartWalletConfig: {
@@ -187,10 +193,12 @@ describe('Verbs SDK - System Tests', () => {
187193
hostedWalletConfig: {
188194
provider: {
189195
type: 'privy',
190-
privyClient: createMockPrivyClient(
191-
'test-app-id',
192-
'test-app-secret',
193-
),
196+
config: {
197+
privyClient: createMockPrivyClient(
198+
'test-app-id',
199+
'test-app-secret',
200+
),
201+
},
194202
},
195203
},
196204
smartWalletConfig: {
@@ -222,10 +230,12 @@ describe('Verbs SDK - System Tests', () => {
222230
hostedWalletConfig: {
223231
provider: {
224232
type: 'privy',
225-
privyClient: createMockPrivyClient(
226-
'test-app-id',
227-
'test-app-secret',
228-
),
233+
config: {
234+
privyClient: createMockPrivyClient(
235+
'test-app-id',
236+
'test-app-secret',
237+
),
238+
},
229239
},
230240
},
231241
smartWalletConfig: {

packages/sdk/src/verbs.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { VerbsConfig } from '@/types/verbs.js'
55
import type { HostedWalletProvider } from '@/wallet/providers/base/HostedWalletProvider.js'
66
import type { SmartWalletProvider } from '@/wallet/providers/base/SmartWalletProvider.js'
77
import { DefaultSmartWalletProvider } from '@/wallet/providers/DefaultSmartWalletProvider.js'
8-
import { PrivyHostedWalletProvider } from '@/wallet/providers/PrivyHostedWalletProvider.js'
8+
import { HostedWalletProviderRegistry } from '@/wallet/providers/HostedWalletProviderRegistry.js'
99
import { WalletNamespace } from '@/wallet/WalletNamespace.js'
1010
import { WalletProvider } from '@/wallet/WalletProvider.js'
1111

@@ -19,9 +19,11 @@ export class Verbs {
1919
private lendProvider?: LendProvider
2020
private hostedWalletProvider!: HostedWalletProvider
2121
private smartWalletProvider!: SmartWalletProvider
22+
private hostedWalletProviderRegistry: HostedWalletProviderRegistry
2223

2324
constructor(config: VerbsConfig) {
2425
this.chainManager = new ChainManager(config.chains)
26+
this.hostedWalletProviderRegistry = new HostedWalletProviderRegistry()
2527

2628
// Create lending provider if configured
2729
if (config.lend) {
@@ -57,16 +59,19 @@ export class Verbs {
5759
* @returns WalletProvider instance
5860
*/
5961
private createWalletProvider(config: VerbsConfig['wallet']) {
60-
if (config.hostedWalletConfig.provider.type === 'privy') {
61-
this.hostedWalletProvider = new PrivyHostedWalletProvider(
62-
config.hostedWalletConfig.provider.privyClient,
63-
this.chainManager,
64-
)
65-
} else {
62+
const hostedWalletProviderConfig = config.hostedWalletConfig.provider
63+
const factory = this.hostedWalletProviderRegistry.getFactory(
64+
hostedWalletProviderConfig.type,
65+
)
66+
if (!factory.validateOptions(hostedWalletProviderConfig.config)) {
6667
throw new Error(
67-
`Unsupported hosted wallet provider: ${config.hostedWalletConfig.provider.type}`,
68+
`Invalid options for hosted wallet provider: ${hostedWalletProviderConfig.type}`,
6869
)
6970
}
71+
this.hostedWalletProvider = factory.create(
72+
{ chainManager: this.chainManager },
73+
hostedWalletProviderConfig.config,
74+
)
7075

7176
if (
7277
!config.smartWalletConfig ||
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { PrivyClient } from '@privy-io/server-auth'
2+
import { unichain } from 'viem/chains'
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import type { ChainManager } from '@/services/ChainManager.js'
6+
import { MockChainManager } from '@/test/MockChainManager.js'
7+
import { createMockPrivyClient } from '@/test/MockPrivyClient.js'
8+
import { HostedWalletProviderRegistry } from '@/wallet/providers/HostedWalletProviderRegistry.js'
9+
import { PrivyHostedWalletProvider } from '@/wallet/providers/PrivyHostedWalletProvider.js'
10+
11+
describe('HostedWalletProviderRegistry', () => {
12+
const mockChainManager = new MockChainManager({
13+
supportedChains: [unichain.id],
14+
}) as unknown as ChainManager
15+
let mockPrivyClient: PrivyClient
16+
17+
beforeEach(() => {
18+
mockPrivyClient = createMockPrivyClient('test-app-id', 'test-app-secret')
19+
})
20+
21+
afterEach(() => {
22+
vi.clearAllMocks()
23+
})
24+
25+
it('returns privy factory and validates options', () => {
26+
const registry = new HostedWalletProviderRegistry()
27+
const factory = registry.getFactory('privy')
28+
29+
expect(factory.type).toBe('privy')
30+
expect(factory.validateOptions?.({ privyClient: mockPrivyClient })).toBe(
31+
true,
32+
)
33+
// Invalid shape should not pass validation
34+
expect(factory.validateOptions?.({})).toBe(false)
35+
})
36+
37+
it('creates a PrivyHostedWalletProvider instance', () => {
38+
const registry = new HostedWalletProviderRegistry()
39+
const factory = registry.getFactory('privy')
40+
41+
const provider = factory.create(
42+
{ chainManager: mockChainManager },
43+
{ privyClient: mockPrivyClient },
44+
)
45+
46+
expect(provider).toBeInstanceOf(PrivyHostedWalletProvider)
47+
})
48+
49+
it('throws for unknown provider type', () => {
50+
const registry = new HostedWalletProviderRegistry()
51+
// @ts-expect-error: testing runtime error for unknown type
52+
expect(() => registry.getFactory('unknown')).toThrow(
53+
'Unknown hosted wallet provider: unknown',
54+
)
55+
})
56+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type {
2+
HostedProviderFactory,
3+
HostedProviderType,
4+
PrivyOptions,
5+
} from '@/wallet/providers/hostedProvider.types.js'
6+
import { PrivyHostedWalletProvider } from '@/wallet/providers/PrivyHostedWalletProvider.js'
7+
8+
export class HostedWalletProviderRegistry {
9+
private readonly registry = new Map<
10+
HostedProviderType,
11+
HostedProviderFactory
12+
>()
13+
14+
public constructor() {
15+
this.register<'privy'>({
16+
type: 'privy',
17+
validateOptions(options): options is PrivyOptions {
18+
return Boolean((options as PrivyOptions)?.privyClient)
19+
},
20+
create({ chainManager }, options) {
21+
return new PrivyHostedWalletProvider(options.privyClient, chainManager)
22+
},
23+
})
24+
}
25+
26+
getFactory<TType extends HostedProviderType>(type: TType) {
27+
const factory = this.registry.get(type) as
28+
| HostedProviderFactory<TType>
29+
| undefined
30+
if (!factory) throw new Error(`Unknown hosted wallet provider: ${type}`)
31+
return factory
32+
}
33+
34+
private register<T extends HostedProviderType>(
35+
factory: HostedProviderFactory<T>,
36+
) {
37+
if (!this.registry.has(factory.type))
38+
this.registry.set(factory.type, factory)
39+
}
40+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { PrivyClient } from '@privy-io/server-auth'
2+
3+
import type { ChainManager } from '@/services/ChainManager.js'
4+
import type { HostedWalletProvider } from '@/wallet/providers/base/HostedWalletProvider.js'
5+
6+
export interface PrivyOptions {
7+
privyClient: PrivyClient
8+
}
9+
10+
export interface HostedProviderConfigMap {
11+
privy: PrivyOptions
12+
}
13+
14+
export type HostedProviderType = keyof HostedProviderConfigMap
15+
16+
export interface HostedProviderDeps {
17+
chainManager: ChainManager
18+
}
19+
20+
export interface HostedProviderFactory<
21+
TType extends HostedProviderType = HostedProviderType,
22+
TOptions = HostedProviderConfigMap[TType],
23+
> {
24+
type: TType
25+
validateOptions(options: unknown): options is TOptions
26+
create(deps: HostedProviderDeps, options: TOptions): HostedWalletProvider
27+
}
28+
29+
export type HostedProviderUnion = {
30+
[K in HostedProviderType]: { type: K; config: HostedProviderConfigMap[K] }
31+
}[HostedProviderType]

0 commit comments

Comments
 (0)