@hub3js/core 0.0.0-experimental-03bf1f89-20260602

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +381 -0
  2. package/dist/builders/action.d.ts +22 -0
  3. package/dist/builders/action.d.ts.map +1 -0
  4. package/dist/builders/mod.d.ts +5 -0
  5. package/dist/builders/mod.d.ts.map +1 -0
  6. package/dist/builders/namespace.d.ts +47 -0
  7. package/dist/builders/namespace.d.ts.map +1 -0
  8. package/dist/builders/provider.d.ts +37 -0
  9. package/dist/builders/provider.d.ts.map +1 -0
  10. package/dist/builders/types.d.ts +23 -0
  11. package/dist/builders/types.d.ts.map +1 -0
  12. package/dist/hub/helpers.d.ts +6 -0
  13. package/dist/hub/helpers.d.ts.map +1 -0
  14. package/dist/hub/hub.d.ts +44 -0
  15. package/dist/hub/hub.d.ts.map +1 -0
  16. package/dist/hub/mod.d.ts +11 -0
  17. package/dist/hub/mod.d.ts.map +1 -0
  18. package/dist/hub/namespaces/errors.d.ts +5 -0
  19. package/dist/hub/namespaces/errors.d.ts.map +1 -0
  20. package/dist/hub/namespaces/mod.d.ts +3 -0
  21. package/dist/hub/namespaces/mod.d.ts.map +1 -0
  22. package/dist/hub/namespaces/namespace.d.ts +148 -0
  23. package/dist/hub/namespaces/namespace.d.ts.map +1 -0
  24. package/dist/hub/namespaces/types.d.ts +36 -0
  25. package/dist/hub/namespaces/types.d.ts.map +1 -0
  26. package/dist/hub/provider/mod.d.ts +3 -0
  27. package/dist/hub/provider/mod.d.ts.map +1 -0
  28. package/dist/hub/provider/provider.d.ts +102 -0
  29. package/dist/hub/provider/provider.d.ts.map +1 -0
  30. package/dist/hub/provider/types.d.ts +28 -0
  31. package/dist/hub/provider/types.d.ts.map +1 -0
  32. package/dist/hub/store/events.d.ts +54 -0
  33. package/dist/hub/store/events.d.ts.map +1 -0
  34. package/dist/hub/store/extend.d.ts +14 -0
  35. package/dist/hub/store/extend.d.ts.map +1 -0
  36. package/dist/hub/store/hub.d.ts +11 -0
  37. package/dist/hub/store/hub.d.ts.map +1 -0
  38. package/dist/hub/store/mod.d.ts +8 -0
  39. package/dist/hub/store/mod.d.ts.map +1 -0
  40. package/dist/hub/store/mod.js +2 -0
  41. package/dist/hub/store/mod.js.map +7 -0
  42. package/dist/hub/store/namespaces.d.ts +38 -0
  43. package/dist/hub/store/namespaces.d.ts.map +1 -0
  44. package/dist/hub/store/providers.d.ts +84 -0
  45. package/dist/hub/store/providers.d.ts.map +1 -0
  46. package/dist/hub/store/selectors.d.ts +18 -0
  47. package/dist/hub/store/selectors.d.ts.map +1 -0
  48. package/dist/hub/store/store.d.ts +14 -0
  49. package/dist/hub/store/store.d.ts.map +1 -0
  50. package/dist/hub/types.d.ts +5 -0
  51. package/dist/hub/types.d.ts.map +1 -0
  52. package/dist/mod.d.ts +5 -0
  53. package/dist/mod.d.ts.map +1 -0
  54. package/dist/mod.js +2 -0
  55. package/dist/mod.js.map +7 -0
  56. package/dist/test-utils/fixtures.d.ts +4 -0
  57. package/dist/test-utils/fixtures.d.ts.map +1 -0
  58. package/dist/test-utils/mod.d.ts +2 -0
  59. package/dist/test-utils/mod.d.ts.map +1 -0
  60. package/dist/test-utils/mod.js +2 -0
  61. package/dist/test-utils/mod.js.map +7 -0
  62. package/dist/utils/mod.d.ts +3 -0
  63. package/dist/utils/mod.d.ts.map +1 -0
  64. package/dist/utils/mod.js +2 -0
  65. package/dist/utils/mod.js.map +7 -0
  66. package/dist/utils/versions.d.ts +50 -0
  67. package/dist/utils/versions.d.ts.map +1 -0
  68. package/package.json +49 -0
  69. package/readme.md +4 -0
  70. package/src/builders/action.test.ts +225 -0
  71. package/src/builders/action.ts +86 -0
  72. package/src/builders/mod.ts +5 -0
  73. package/src/builders/namespace.ts +258 -0
  74. package/src/builders/provider.ts +101 -0
  75. package/src/builders/types.ts +28 -0
  76. package/src/hub/helpers.ts +11 -0
  77. package/src/hub/hub.test.ts +108 -0
  78. package/src/hub/hub.ts +145 -0
  79. package/src/hub/mod.ts +21 -0
  80. package/src/hub/namespaces/errors.ts +11 -0
  81. package/src/hub/namespaces/mod.ts +10 -0
  82. package/src/hub/namespaces/namespace.test.ts +443 -0
  83. package/src/hub/namespaces/namespace.ts +470 -0
  84. package/src/hub/namespaces/types.ts +58 -0
  85. package/src/hub/provider/mod.ts +9 -0
  86. package/src/hub/provider/provider.test.ts +277 -0
  87. package/src/hub/provider/provider.ts +286 -0
  88. package/src/hub/provider/types.ts +37 -0
  89. package/src/hub/store/events.ts +89 -0
  90. package/src/hub/store/extend.ts +125 -0
  91. package/src/hub/store/hub.ts +18 -0
  92. package/src/hub/store/mod.ts +26 -0
  93. package/src/hub/store/namespaces.ts +177 -0
  94. package/src/hub/store/providers.ts +183 -0
  95. package/src/hub/store/selectors.ts +59 -0
  96. package/src/hub/store/store.test.ts +50 -0
  97. package/src/hub/store/store.ts +30 -0
  98. package/src/hub/types.ts +13 -0
  99. package/src/mod.ts +27 -0
  100. package/src/test-utils/fixtures.ts +13 -0
  101. package/src/test-utils/mod.ts +1 -0
  102. package/src/utils/mod.ts +2 -0
  103. package/src/utils/versions.test.ts +23 -0
  104. package/src/utils/versions.ts +91 -0
  105. package/store/package.json +8 -0
@@ -0,0 +1,225 @@
1
+ import type { Context } from '../hub/namespaces/mod.js';
2
+ import type { FunctionWithContext } from '../hub/types.js';
3
+
4
+ import { describe, expect, test, vi } from 'vitest';
5
+
6
+ import { garbageWalletMetaData } from '../test-utils/fixtures.js';
7
+
8
+ import { ActionBuilder } from './action.js';
9
+ import { NamespaceBuilder } from './namespace.js';
10
+ import { ProviderBuilder } from './provider.js';
11
+
12
+ interface TestEvmActions {
13
+ connect: (chain: string) => Promise<{ accounts: string[]; network: string }>;
14
+ disconnect: () => void;
15
+ }
16
+
17
+ interface TestSolanaActions {
18
+ connect: () => Promise<string[]>;
19
+ }
20
+
21
+ describe('Provider built from multiple namespaces', () => {
22
+ const walletName = 'garbage-wallet';
23
+
24
+ test('connect runs the right action on each registered namespace', async () => {
25
+ const spyOnEvmConnect = vi.fn();
26
+ const evmConnect: FunctionWithContext<
27
+ TestEvmActions['connect'],
28
+ Context
29
+ > = async () => {
30
+ spyOnEvmConnect();
31
+ return {
32
+ accounts: ['eip155:0x1:0x000000000000000000000000000000000000dead'],
33
+ network: 'eth',
34
+ };
35
+ };
36
+
37
+ const spyOnSolanaConnect = vi.fn();
38
+ const solanaConnect: FunctionWithContext<
39
+ TestSolanaActions['connect'],
40
+ Context
41
+ > = async () => {
42
+ spyOnSolanaConnect();
43
+ return ['solana:mainnet:1nc1nerator11111111111111111111111111111111'];
44
+ };
45
+
46
+ const evmNamespace = new NamespaceBuilder<TestEvmActions>('evm', walletName)
47
+ .action('connect', evmConnect)
48
+ .build();
49
+ const solanaNamespace = new NamespaceBuilder<TestSolanaActions>(
50
+ 'solana',
51
+ walletName,
52
+ )
53
+ .action('connect', solanaConnect)
54
+ .build();
55
+
56
+ const wallet = new ProviderBuilder(walletName)
57
+ .add('evm', evmNamespace)
58
+ .add('solana', solanaNamespace)
59
+ .config('metadata', garbageWalletMetaData)
60
+ .build();
61
+
62
+ const evmResult = await wallet.get('evm')?.connect('0x1');
63
+ const solanaResult = await wallet.get('solana')?.connect();
64
+
65
+ expect(evmResult?.accounts).toStrictEqual([
66
+ 'eip155:0x1:0x000000000000000000000000000000000000dead',
67
+ ]);
68
+ expect(solanaResult).toStrictEqual([
69
+ 'solana:mainnet:1nc1nerator11111111111111111111111111111111',
70
+ ]);
71
+ expect(spyOnEvmConnect).toBeCalledTimes(1);
72
+ expect(spyOnSolanaConnect).toBeCalledTimes(1);
73
+ });
74
+ });
75
+
76
+ describe('ActionBuilder integration with namespace', () => {
77
+ const walletName = 'garbage-wallet';
78
+
79
+ test('and-then transforms the action result; after hook runs', async () => {
80
+ const spyOnConnect = vi.fn();
81
+ const evmConnect: FunctionWithContext<
82
+ TestEvmActions['connect'],
83
+ Context
84
+ > = async () => {
85
+ spyOnConnect();
86
+ return {
87
+ accounts: [
88
+ '0x000000000000000000000000000000000000dead',
89
+ '0x0000000000000000000000000000000000000000',
90
+ ],
91
+ network: 'eth',
92
+ };
93
+ };
94
+
95
+ const andConnect = vi.fn(
96
+ (_context, result: { network: string; accounts: string[] }) => ({
97
+ network: result.network,
98
+ accounts: result.accounts.map((account) => `eip155:0x1:${account}`),
99
+ }),
100
+ );
101
+
102
+ const evmDisconnect = vi.fn();
103
+ const afterDisconnect = vi.fn();
104
+
105
+ const connectAction = new ActionBuilder<TestEvmActions, 'connect'>(
106
+ 'connect',
107
+ )
108
+ .action(evmConnect)
109
+ .and(andConnect)
110
+ .build();
111
+
112
+ const disconnectAction = new ActionBuilder<TestEvmActions, 'disconnect'>(
113
+ 'disconnect',
114
+ )
115
+ .action(evmDisconnect)
116
+ .after(afterDisconnect)
117
+ .build();
118
+
119
+ const evmNamespace = new NamespaceBuilder<TestEvmActions>('evm', walletName)
120
+ .action(connectAction)
121
+ .action(disconnectAction)
122
+ .build();
123
+
124
+ const wallet = new ProviderBuilder(walletName)
125
+ .add('evm', evmNamespace)
126
+ .config('metadata', garbageWalletMetaData)
127
+ .build();
128
+
129
+ const evmResult = await wallet.get('evm')?.connect('0x1');
130
+
131
+ expect(evmResult?.accounts).toStrictEqual([
132
+ 'eip155:0x1:0x000000000000000000000000000000000000dead',
133
+ 'eip155:0x1:0x0000000000000000000000000000000000000000',
134
+ ]);
135
+
136
+ await wallet.get('evm')?.connect('0x1');
137
+ wallet.get('evm')?.disconnect();
138
+
139
+ expect(spyOnConnect).toBeCalledTimes(2);
140
+ expect(andConnect).toBeCalledTimes(2);
141
+ expect(evmDisconnect).toBeCalledTimes(1);
142
+ expect(afterDisconnect).toBeCalledTimes(1);
143
+ });
144
+
145
+ test('and runs on success; or runs on throw; throw without or propagates', async () => {
146
+ const spyOnSuccessAndAction = vi.fn((_ctx, value) => value);
147
+ const spyOnThrowAndAction = vi.fn();
148
+ const spyOnThrowAndActionWithOr = vi.fn();
149
+ const spyOnSuccessOrAction = vi.fn();
150
+ const spyOnThrowOrAction = vi.fn();
151
+
152
+ interface GarbageActions {
153
+ successfulAction: () => string;
154
+ throwErrorAction: () => void;
155
+ throwErrorActionWithOr: () => void;
156
+ }
157
+
158
+ const successfulAction = new ActionBuilder<
159
+ GarbageActions,
160
+ 'successfulAction'
161
+ >('successfulAction')
162
+ .action(() => 'yay!')
163
+ .and(spyOnSuccessAndAction)
164
+ .or(spyOnSuccessOrAction)
165
+ .build();
166
+
167
+ const throwErrorAction = new ActionBuilder<
168
+ GarbageActions,
169
+ 'throwErrorAction'
170
+ >('throwErrorAction')
171
+ .action(() => {
172
+ throw new Error('whatever');
173
+ })
174
+ .and(spyOnThrowAndAction)
175
+ .build();
176
+
177
+ const throwErrorActionWithOr = new ActionBuilder<
178
+ GarbageActions,
179
+ 'throwErrorActionWithOr'
180
+ >('throwErrorActionWithOr')
181
+ .action(() => {
182
+ throw new Error('whatever');
183
+ })
184
+ .and(spyOnThrowAndActionWithOr)
185
+ .or(spyOnThrowOrAction)
186
+ .build();
187
+
188
+ const namespace = new NamespaceBuilder<GarbageActions>('evm', walletName)
189
+ .action(successfulAction)
190
+ .action(throwErrorAction)
191
+ .action(throwErrorActionWithOr)
192
+ .build();
193
+
194
+ namespace.successfulAction();
195
+ expect(spyOnSuccessAndAction).toBeCalledTimes(1);
196
+ expect(spyOnSuccessAndAction).toHaveLastReturnedWith('yay!');
197
+ expect(spyOnSuccessOrAction).toBeCalledTimes(0);
198
+
199
+ expect(() => namespace.throwErrorAction()).toThrowError();
200
+ expect(spyOnThrowAndAction).toBeCalledTimes(0);
201
+
202
+ namespace.throwErrorActionWithOr();
203
+ expect(spyOnThrowAndActionWithOr).toBeCalledTimes(0);
204
+ expect(spyOnThrowOrAction).toBeCalledTimes(1);
205
+ });
206
+
207
+ test("'action-name' in namespace returns true for registered actions", () => {
208
+ interface GarbageActions {
209
+ garbageAction: () => string;
210
+ }
211
+
212
+ const garbageAction = new ActionBuilder<GarbageActions, 'garbageAction'>(
213
+ 'garbageAction',
214
+ )
215
+ .action(() => 'yay!')
216
+ .build();
217
+
218
+ const namespace = new NamespaceBuilder<GarbageActions>('evm', walletName)
219
+ .action(garbageAction)
220
+ .build();
221
+
222
+ expect('garbageAction' in namespace).toBe(true);
223
+ expect('init' in namespace).toBe(true);
224
+ });
225
+ });
@@ -0,0 +1,86 @@
1
+ import type { Actions, Context, Operators } from '../hub/namespaces/types.js';
2
+ import type { AnyFunction, FunctionWithContext } from '../hub/types.js';
3
+
4
+ export interface ActionByBuilder<T, Context> {
5
+ actionName: keyof T;
6
+ and: Operators<T>;
7
+ or: Operators<T>;
8
+ after: Operators<T>;
9
+ before: Operators<T>;
10
+ action: FunctionWithContext<T[keyof T], Context>;
11
+ }
12
+
13
+ /*
14
+ * TODO:
15
+ * Currently, to use this builder you will write something like this:
16
+ * new ActionBuilder<EvmActions, 'disconnect'>('disconnect').after(....)
17
+ *
18
+ * I couldn't figure it out to be able typescript infer the constructor value as key of actions.
19
+ * Ideal usage:
20
+ * new ActionBuilder<EvmActions>('disconnect').after(....)
21
+ *
22
+ */
23
+ export class ActionBuilder<T extends Actions<T>, K extends keyof T> {
24
+ readonly name: K;
25
+ #and: Operators<T> = new Map();
26
+ #or: Operators<T> = new Map();
27
+ #after: Operators<T> = new Map();
28
+ #before: Operators<T> = new Map();
29
+ #action: FunctionWithContext<T[keyof T], Context<T>> | undefined;
30
+
31
+ constructor(name: K) {
32
+ this.name = name;
33
+ }
34
+
35
+ public and(action: FunctionWithContext<AnyFunction, Context<T>>) {
36
+ if (!this.#and.has(this.name)) {
37
+ this.#and.set(this.name, []);
38
+ }
39
+ this.#and.get(this.name)?.push(action);
40
+ return this;
41
+ }
42
+
43
+ public or(action: FunctionWithContext<AnyFunction, Context<T>>) {
44
+ if (!this.#or.has(this.name)) {
45
+ this.#or.set(this.name, []);
46
+ }
47
+ this.#or.get(this.name)?.push(action);
48
+ return this;
49
+ }
50
+
51
+ public before(action: FunctionWithContext<AnyFunction, Context<T>>) {
52
+ if (!this.#before.has(this.name)) {
53
+ this.#before.set(this.name, []);
54
+ }
55
+ this.#before.get(this.name)?.push(action);
56
+ return this;
57
+ }
58
+
59
+ public after(action: FunctionWithContext<AnyFunction, Context<T>>) {
60
+ if (!this.#after.has(this.name)) {
61
+ this.#after.set(this.name, []);
62
+ }
63
+ this.#after.get(this.name)?.push(action);
64
+ return this;
65
+ }
66
+
67
+ public action(action: FunctionWithContext<T[keyof T], Context<T>>) {
68
+ this.#action = action;
69
+ return this;
70
+ }
71
+
72
+ public build(): ActionByBuilder<T, Context<T>> {
73
+ if (!this.#action) {
74
+ throw new Error('Your action builder should includes an action.');
75
+ }
76
+
77
+ return {
78
+ actionName: this.name,
79
+ action: this.#action,
80
+ before: this.#before,
81
+ after: this.#after,
82
+ and: this.#and,
83
+ or: this.#or,
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,5 @@
1
+ export type { ProxiedNamespace, FindProxiedNamespace } from './types.js';
2
+
3
+ export { NamespaceBuilder } from './namespace.js';
4
+ export { ProviderBuilder } from './provider.js';
5
+ export { ActionBuilder } from './action.js';
@@ -0,0 +1,258 @@
1
+ import type { ActionByBuilder } from './action.js';
2
+ import type { ProxiedNamespace } from './types.js';
3
+ import type { Actions, ActionsMap, Context } from '../hub/namespaces/mod.js';
4
+ import type { NamespaceConfig } from '../hub/store/mod.js';
5
+ import type { FunctionWithContext } from '../hub/types.js';
6
+
7
+ import { Namespace } from '../hub/mod.js';
8
+
9
+ /**
10
+ * There are Namespace's methods that should be called directly on Proxy object.
11
+ * The Proxy object is creating in `.build`.
12
+ */
13
+ export const allowedMethods = [
14
+ 'init',
15
+ 'state',
16
+ 'after',
17
+ 'before',
18
+ 'and_then',
19
+ 'or_else',
20
+ 'store',
21
+ ] as const;
22
+ /**
23
+ * List of value types that are considered safe to return directly when
24
+ * accessing properties on a proxied `Namespace`.
25
+ *
26
+ * This is useful for allowing public values like `version` to be accessed.
27
+ * If a property's value is of one of these types (e.g.,`'string'`, `'number'`),
28
+ * it will be returned as-is.
29
+ *
30
+ * @const {Array<'string' | 'number'>} allowedPublicValues
31
+ */
32
+ const allowedPublicValues = ['string', 'number'];
33
+
34
+ export class NamespaceBuilder<T extends Actions<T>> {
35
+ #id: string;
36
+ #providerId: string;
37
+ #actions: ActionsMap<T> = new Map();
38
+ /*
39
+ * We keep a list of `ActionBuilder` outputs here to use them in separate phases.
40
+ * Actually, `ActionBuilder` is packing action and its hooks in one place, here we should expand them and them in appropriate places.
41
+ * Eventually, action will be added to `#actions` and its hooks will be added to `Namespace`.
42
+ */
43
+ #actionBuilders: ActionByBuilder<T, Context<T>>[] = [];
44
+ #configs: NamespaceConfig;
45
+
46
+ constructor(id: string, providerId: string) {
47
+ this.#id = id;
48
+ this.#providerId = providerId;
49
+ this.#configs = {};
50
+ }
51
+
52
+ /** There are some predefined configs that can be set for each namespace separately */
53
+ public config<K extends keyof NamespaceConfig>(
54
+ name: K,
55
+ value: NamespaceConfig[K],
56
+ ) {
57
+ this.#configs[name] = value;
58
+ return this;
59
+ }
60
+
61
+ /**
62
+ * Getting a list of actions.
63
+ *
64
+ * e.g.:
65
+ * ```ts
66
+ * .action([
67
+ * ["connect", () => {}],
68
+ * ["disconnect", () => {}]
69
+ * ])
70
+ * ```
71
+ *
72
+ */
73
+ public action<K extends keyof T>(
74
+ action: (readonly [K, FunctionWithContext<T[K], Context<T>>])[],
75
+ ): NamespaceBuilder<T>;
76
+
77
+ /**
78
+ *
79
+ * Add a single action
80
+ *
81
+ * e.g.:
82
+ * ```ts
83
+ * .action( ["connect", () => {}] )
84
+ * ```
85
+ */
86
+ public action<K extends keyof T>(
87
+ action: K,
88
+ actionFn: FunctionWithContext<T[K], Context<T>>,
89
+ ): NamespaceBuilder<T>;
90
+
91
+ public action(action: ActionByBuilder<T, Context<T>>): NamespaceBuilder<T>;
92
+
93
+ /**
94
+ *
95
+ * Actions are piece of functionality that a namespace can have, for example it can be a `connect` function
96
+ * or a sign function or even a function for updating namespace's internal state. Actions are flexible and can be anything.
97
+ *
98
+ * Generally, each standard namespace (e.g. evm) has an standard interface defined in `src/namespaces/`
99
+ * and provider (which includes namespaces) authors will implement those actions.
100
+ *
101
+ * You can call this function by a list of actions or a single action.
102
+ *
103
+ */
104
+ public action<K extends keyof T>(
105
+ action: (readonly [K, FunctionWithContext<T[K], Context<T>>])[] | K,
106
+ actionFn?: FunctionWithContext<T[K], Context<T>>,
107
+ ) {
108
+ // List mode
109
+ if (Array.isArray(action)) {
110
+ action.forEach(([name, actionFnForItem]) => {
111
+ this.#actions.set(name, actionFnForItem);
112
+ });
113
+ return this;
114
+ }
115
+
116
+ // Action builder mode
117
+
118
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
119
+ // @ts-ignore
120
+ if (typeof action === 'object' && !!action?.actionName) {
121
+ this.#actionBuilders.push(action);
122
+ return this;
123
+ }
124
+
125
+ // Single action mode
126
+ if (!!actionFn) {
127
+ this.#actions.set(action, actionFn);
128
+ }
129
+
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * By calling build, an instance of Namespace will be built.
135
+ *
136
+ * Note: it's not exactly a `Namespace`, it returns a Proxy which add more convenient use like `namespace.connect()` instead of `namespace.run("connect")`
137
+ */
138
+ public build(): ProxiedNamespace<T> {
139
+ if (this.#isConfigsValid(this.#configs)) {
140
+ return this.#buildApi(this.#configs);
141
+ }
142
+
143
+ throw new Error(`You namespace config isn't valid.`);
144
+ }
145
+
146
+ // Currently, namespace doesn't has any config.
147
+ #isConfigsValid(_config: NamespaceConfig): boolean {
148
+ return true;
149
+ }
150
+
151
+ /*
152
+ * Extracting hooks and add them to `Namespace` for the action.
153
+ *
154
+ * Note: this should be called after `addActionsFromActionBuilders` to ensure the action is added first.
155
+ */
156
+ #addHooksFromActionBuilders(namespace: Namespace<T>) {
157
+ this.#actionBuilders.forEach((actionByBuild) => {
158
+ actionByBuild.after.forEach((afterHooks) => {
159
+ afterHooks.map((action) => {
160
+ namespace.after(actionByBuild.actionName, action);
161
+ });
162
+ });
163
+
164
+ actionByBuild.before.forEach((beforeHooks) => {
165
+ beforeHooks.map((action) => {
166
+ namespace.before(actionByBuild.actionName, action);
167
+ });
168
+ });
169
+
170
+ actionByBuild.and.forEach((andHooks) => {
171
+ andHooks.map((action) => {
172
+ namespace.and_then(actionByBuild.actionName, action);
173
+ });
174
+ });
175
+
176
+ actionByBuild.or.forEach((orHooks) => {
177
+ orHooks.map((action) => {
178
+ namespace.or_else(actionByBuild.actionName, action);
179
+ });
180
+ });
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Iterate over `actionBuilders` and add them to exists `actions`.
186
+ * Note: Hooks will be added in a separate phase.
187
+ */
188
+ #addActionsFromActionBuilders() {
189
+ this.#actionBuilders.forEach((actionByBuild) => {
190
+ this.#actions.set(actionByBuild.actionName, actionByBuild.action);
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Build a Proxy object to call actions in a more convenient way. e.g `.connect()` instead of `.run(connect)`
196
+ */
197
+ #buildApi(configs: NamespaceConfig): ProxiedNamespace<T> {
198
+ this.#addActionsFromActionBuilders();
199
+ const namespace = new Namespace<T>(this.#id, this.#providerId, {
200
+ configs,
201
+ actions: this.#actions,
202
+ });
203
+ this.#addHooksFromActionBuilders(namespace);
204
+
205
+ const api = new Proxy(namespace, {
206
+ has: (_, property) => {
207
+ if (typeof property !== 'string') {
208
+ throw new Error(
209
+ 'You can use string as your property on Namespace instance.',
210
+ );
211
+ }
212
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
213
+ // @ts-ignore-next-line
214
+ if (allowedMethods.includes(property)) {
215
+ return true;
216
+ }
217
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
218
+ // @ts-ignore-next-line
219
+ if (allowedPublicValues.includes(typeof namespace[property])) {
220
+ return true;
221
+ }
222
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
223
+ // @ts-ignore-next-line
224
+ return namespace.has(property);
225
+ },
226
+ get: (_, property) => {
227
+ if (typeof property !== 'string') {
228
+ throw new Error(
229
+ 'You can use string as your property on Namespace instance.',
230
+ );
231
+ }
232
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
233
+ // @ts-ignore-next-line
234
+ const targetValue = namespace[property];
235
+
236
+ if (
237
+ allowedMethods.includes(property as (typeof allowedMethods)[number])
238
+ ) {
239
+ return targetValue.bind(namespace);
240
+ }
241
+ /*
242
+ * This is useful accessing values like `version`, If we don't do this, we should whitelist
243
+ * All the values as well, So it can be confusing for someone that only wants to add a public value to `Namespace`
244
+ */
245
+ if (allowedPublicValues.includes(typeof targetValue)) {
246
+ return targetValue;
247
+ }
248
+
249
+ return namespace.run.bind(namespace, property as keyof T);
250
+ },
251
+ set: () => {
252
+ throw new Error('You can not set anything on this object.');
253
+ },
254
+ });
255
+
256
+ return api as unknown as ProxiedNamespace<T>;
257
+ }
258
+ }
@@ -0,0 +1,101 @@
1
+ import type { ProxiedNamespace } from './types.js';
2
+ import type { Actions } from '../hub/namespaces/mod.js';
3
+ import type {
4
+ ExtendableInternalActions,
5
+ ProviderBuilderOptions,
6
+ RegisteredNamespaces,
7
+ } from '../hub/provider/mod.js';
8
+ import type { ProviderConfig } from '../hub/store/mod.js';
9
+
10
+ import { Provider } from '../hub/provider/mod.js';
11
+
12
+ /**
13
+ * Fluent builder for `Provider`. Each `.add(key, namespace)` call accumulates
14
+ * the registered namespace map into the `TNS` generic, so the resulting
15
+ * `Provider<TNS>` knows exactly which namespaces it owns.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const phantom = new ProviderBuilder('phantom')
20
+ * .config('metadata', meta)
21
+ * .add('evm', evmNamespace) // adds { evm: EvmActions }
22
+ * .add('solana', solanaNamespace) // adds { solana: SolanaActions }
23
+ * .build();
24
+ * // Provider<{ evm: EvmActions; solana: SolanaActions }>
25
+ * ```
26
+ * `{}` is intentional: identity element under intersection so the first
27
+ * `.add(K, T)` produces `{} & Record<K, T> = Record<K, T>` and subsequent calls
28
+ * accumulate cleanly. Not the unsafe "any non-nullish" usage the rule targets.
29
+ */
30
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
31
+ export class ProviderBuilder<TNS = {}> {
32
+ #id: string;
33
+ /*
34
+ * Internal storage is opaque — types are recovered when `build()` produces
35
+ * the `Provider<TNS>`.
36
+ */
37
+ #namespaces = new Map<string, unknown>();
38
+ #methods: ExtendableInternalActions = {};
39
+ #configs: Partial<ProviderConfig<TNS>> = {};
40
+ #options: Partial<ProviderBuilderOptions>;
41
+
42
+ constructor(id: string, options?: ProviderBuilderOptions) {
43
+ this.#id = id;
44
+ this.#options = options || {};
45
+ }
46
+
47
+ /**
48
+ * Register a namespace under `id`. Captures both the key and the namespace's
49
+ * action interface into the builder's `TNS` so subsequent operations are
50
+ * statically typed against the accumulated set.
51
+ */
52
+ public add<const K extends string, T extends Actions<T>>(
53
+ id: K,
54
+ namespace: ProxiedNamespace<T>,
55
+ ): ProviderBuilder<TNS & Record<K, T>> {
56
+ if (this.#options.store) {
57
+ namespace.store(this.#options.store);
58
+ }
59
+ this.#namespaces.set(id, namespace);
60
+ return this as unknown as ProviderBuilder<TNS & Record<K, T>>;
61
+ }
62
+
63
+ public config<K extends keyof ProviderConfig<TNS>>(
64
+ name: K,
65
+ value: ProviderConfig<TNS>[K],
66
+ ): this {
67
+ this.#configs[name] = value;
68
+ return this;
69
+ }
70
+
71
+ public init(cb: Exclude<ExtendableInternalActions['init'], undefined>): this {
72
+ this.#methods.init = cb;
73
+ return this;
74
+ }
75
+
76
+ public build(): Provider<TNS> {
77
+ if (this.#isConfigsValid(this.#configs)) {
78
+ return new Provider<TNS>(
79
+ this.#id,
80
+ /*
81
+ * The runtime map is keyed by `string`; we re-view it through the
82
+ * builder's accumulated `TNS` for the typed `Provider` API.
83
+ */
84
+ this.#namespaces as unknown as RegisteredNamespaces<TNS>,
85
+ this.#configs,
86
+ {
87
+ extendInternalActions: this.#methods,
88
+ store: this.#options.store,
89
+ },
90
+ );
91
+ }
92
+
93
+ throw new Error('You need to set all required configs.');
94
+ }
95
+
96
+ #isConfigsValid(
97
+ config: Partial<ProviderConfig<TNS>>,
98
+ ): config is ProviderConfig<TNS> {
99
+ return !!config.metadata;
100
+ }
101
+ }
@@ -0,0 +1,28 @@
1
+ import type { allowedMethods } from './namespace.js';
2
+ import type { Actions, Namespace } from '../hub/namespaces/mod.js';
3
+
4
+ // These should be matched with `/hub/namespace.ts` public values.
5
+ type NamespacePublicValues = {
6
+ namespaceId: string;
7
+ providerId: string;
8
+ };
9
+
10
+ /**
11
+ * NamespaceBuilder is creating a proxy instead of return Namespace instance.
12
+ * The reason is improving access to actions. e.g `.connect()` instead of `.run('connect')`
13
+ */
14
+ export type ProxiedNamespace<T extends Actions<T>> = T &
15
+ Pick<Namespace<T>, (typeof allowedMethods)[number]> &
16
+ NamespacePublicValues;
17
+
18
+ /**
19
+ * Map a key into the corresponding proxied namespace inside a namespace set.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * type List = { evm: EvmActions; solana: SolanaActions };
24
+ * FindProxiedNamespace<'solana', List>; // ProxiedNamespace<SolanaActions>
25
+ * ```
26
+ */
27
+ export type FindProxiedNamespace<K extends keyof T, T> =
28
+ T[K] extends Actions<T[K]> ? ProxiedNamespace<T[K]> : never;