-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introducing hub, our new wallet management
- Loading branch information
1 parent
a039a5b
commit 5459333
Showing
66 changed files
with
5,278 additions
and
437 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import type { Actions, Context, Operators } from '../hub/namespaces/types.js'; | ||
import type { AnyFunction, FunctionWithContext } from '../types/actions.js'; | ||
|
||
export interface ActionByBuilder<T, Context> { | ||
actionName: keyof T; | ||
and: Operators<T>; | ||
or: Operators<T>; | ||
after: Operators<T>; | ||
before: Operators<T>; | ||
action: FunctionWithContext<T[keyof T], Context>; | ||
} | ||
|
||
/* | ||
* TODO: | ||
* Currently, to use this builder you will write something like this: | ||
* new ActionBuilder<EvmActions, 'disconnect'>('disconnect').after(....) | ||
* | ||
* I couldn't figure it out to be able typescript infer the constructor value as key of actions. | ||
* Ideal usage: | ||
* new ActionBuilder<EvmActions>('disconnect').after(....) | ||
* | ||
*/ | ||
export class ActionBuilder<T extends Actions<T>, K extends keyof T> { | ||
readonly name: K; | ||
#and: Operators<T> = new Map(); | ||
#or: Operators<T> = new Map(); | ||
#after: Operators<T> = new Map(); | ||
#before: Operators<T> = new Map(); | ||
#action: FunctionWithContext<T[keyof T], Context<T>> | undefined; | ||
|
||
constructor(name: K) { | ||
this.name = name; | ||
} | ||
|
||
public and(action: FunctionWithContext<AnyFunction, Context<T>>) { | ||
if (!this.#and.has(this.name)) { | ||
this.#and.set(this.name, []); | ||
} | ||
this.#and.get(this.name)?.push(action); | ||
return this; | ||
} | ||
|
||
public or(action: FunctionWithContext<AnyFunction, Context<T>>) { | ||
if (!this.#or.has(this.name)) { | ||
this.#or.set(this.name, []); | ||
} | ||
this.#or.get(this.name)?.push(action); | ||
return this; | ||
} | ||
|
||
public before(action: FunctionWithContext<AnyFunction, Context<T>>) { | ||
if (!this.#before.has(this.name)) { | ||
this.#before.set(this.name, []); | ||
} | ||
this.#before.get(this.name)?.push(action); | ||
return this; | ||
} | ||
|
||
public after(action: FunctionWithContext<AnyFunction, Context<T>>) { | ||
if (!this.#after.has(this.name)) { | ||
this.#after.set(this.name, []); | ||
} | ||
this.#after.get(this.name)?.push(action); | ||
return this; | ||
} | ||
|
||
public action(action: FunctionWithContext<T[keyof T], Context<T>>) { | ||
this.#action = action; | ||
return this; | ||
} | ||
|
||
public build(): ActionByBuilder<T, Context<T>> { | ||
if (!this.#action) { | ||
throw new Error('Your action builder should includes an action.'); | ||
} | ||
|
||
return { | ||
actionName: this.name, | ||
action: this.#action, | ||
before: this.#before, | ||
after: this.#after, | ||
and: this.#and, | ||
or: this.#or, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export type { ProxiedNamespace, FindProxiedNamespace } from './types.js'; | ||
|
||
export { NamespaceBuilder } from './namespace.js'; | ||
export { ProviderBuilder } from './provider.js'; | ||
export { ActionBuilder } from './action.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
import type { ActionByBuilder } from './action.js'; | ||
import type { ProxiedNamespace } from './types.js'; | ||
import type { Actions, ActionsMap, Context } from '../hub/namespaces/mod.js'; | ||
import type { NamespaceConfig } from '../hub/store/mod.js'; | ||
import type { FunctionWithContext } from '../types/actions.js'; | ||
|
||
import { Namespace } from '../hub/mod.js'; | ||
|
||
/** | ||
* There are Namespace's methods that should be called directly on Proxy object. | ||
* The Proxy object is creating in `.build`. | ||
*/ | ||
export const allowedMethods = [ | ||
'init', | ||
'state', | ||
'after', | ||
'before', | ||
'and_then', | ||
'or_else', | ||
'store', | ||
] as const; | ||
|
||
export class NamespaceBuilder<T extends Actions<T>> { | ||
#id: string; | ||
#providerId: string; | ||
#actions: ActionsMap<T> = new Map(); | ||
/* | ||
* We keep a list of `ActionBuilder` outputs here to use them in separate phases. | ||
* Actually, `ActionBuilder` is packing action and its hooks in one place, here we should expand them and them in appropriate places. | ||
* Eventually, action will be added to `#actions` and its hooks will be added to `Namespace`. | ||
*/ | ||
#actionBuilders: ActionByBuilder<T, Context<T>>[] = []; | ||
#configs: NamespaceConfig; | ||
|
||
constructor(id: string, providerId: string) { | ||
this.#id = id; | ||
this.#providerId = providerId; | ||
this.#configs = {}; | ||
} | ||
|
||
/** There are some predefined configs that can be set for each namespace separately */ | ||
public config<K extends keyof NamespaceConfig>( | ||
name: K, | ||
value: NamespaceConfig[K] | ||
) { | ||
this.#configs[name] = value; | ||
return this; | ||
} | ||
|
||
/** | ||
* Getting a list of actions. | ||
* | ||
* e.g.: | ||
* ```ts | ||
* .action([ | ||
* ["connect", () => {}], | ||
* ["disconnect", () => {}] | ||
* ]) | ||
* ``` | ||
* | ||
*/ | ||
public action<K extends keyof T>( | ||
action: (readonly [K, FunctionWithContext<T[K], Context<T>>])[] | ||
): NamespaceBuilder<T>; | ||
|
||
/** | ||
* | ||
* Add a single action | ||
* | ||
* e.g.: | ||
* ```ts | ||
* .action( ["connect", () => {}] ) | ||
* ``` | ||
*/ | ||
public action<K extends keyof T>( | ||
action: K, | ||
actionFn: FunctionWithContext<T[K], Context<T>> | ||
): NamespaceBuilder<T>; | ||
|
||
public action(action: ActionByBuilder<T, Context<T>>): NamespaceBuilder<T>; | ||
|
||
/** | ||
* | ||
* Actions are piece of functionality that a namespace can have, for example it can be a `connect` function | ||
* or a sign function or even a function for updating namespace's internal state. Actions are flexible and can be anything. | ||
* | ||
* Generally, each standard namespace (e.g. evm) has an standard interface defined in `src/namespaces/` | ||
* and provider (which includes namespaces) authors will implement those actions. | ||
* | ||
* You can call this function by a list of actions or a single action. | ||
* | ||
*/ | ||
public action<K extends keyof T>( | ||
action: (readonly [K, FunctionWithContext<T[K], Context<T>>])[] | K, | ||
actionFn?: FunctionWithContext<T[K], Context<T>> | ||
) { | ||
// List mode | ||
if (Array.isArray(action)) { | ||
action.forEach(([name, actionFnForItem]) => { | ||
this.#actions.set(name, actionFnForItem); | ||
}); | ||
return this; | ||
} | ||
|
||
// Action builder mode | ||
|
||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
if (typeof action === 'object' && !!action?.actionName) { | ||
this.#actionBuilders.push(action); | ||
return this; | ||
} | ||
|
||
// Single action mode | ||
if (!!actionFn) { | ||
this.#actions.set(action, actionFn); | ||
} | ||
|
||
return this; | ||
} | ||
|
||
/** | ||
* By calling build, an instance of Namespace will be built. | ||
* | ||
* Note: it's not exactly a `Namespace`, it returns a Proxy which add more convenient use like `namespace.connect()` instead of `namespace.run("connect")` | ||
*/ | ||
public build(): ProxiedNamespace<T> { | ||
if (this.#isConfigsValid(this.#configs)) { | ||
return this.#buildApi(this.#configs); | ||
} | ||
|
||
throw new Error(`You namespace config isn't valid.`); | ||
} | ||
|
||
// Currently, namespace doesn't has any config. | ||
#isConfigsValid(_config: NamespaceConfig): boolean { | ||
return true; | ||
} | ||
|
||
/* | ||
* Extracting hooks and add them to `Namespace` for the action. | ||
* | ||
* Note: this should be called after `addActionsFromActionBuilders` to ensure the action is added first. | ||
*/ | ||
#addHooksFromActionBuilders(namespace: Namespace<T>) { | ||
this.#actionBuilders.forEach((actionByBuild) => { | ||
actionByBuild.after.forEach((afterHooks) => { | ||
afterHooks.map((action) => { | ||
namespace.after(actionByBuild.actionName, action); | ||
}); | ||
}); | ||
|
||
actionByBuild.before.forEach((beforeHooks) => { | ||
beforeHooks.map((action) => { | ||
namespace.before(actionByBuild.actionName, action); | ||
}); | ||
}); | ||
|
||
actionByBuild.and.forEach((andHooks) => { | ||
andHooks.map((action) => { | ||
namespace.and_then(actionByBuild.actionName, action); | ||
}); | ||
}); | ||
|
||
actionByBuild.or.forEach((orHooks) => { | ||
orHooks.map((action) => { | ||
namespace.or_else(actionByBuild.actionName, action); | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Iterate over `actionBuilders` and add them to exists `actions`. | ||
* Note: Hooks will be added in a separate phase. | ||
*/ | ||
#addActionsFromActionBuilders() { | ||
this.#actionBuilders.forEach((actionByBuild) => { | ||
this.#actions.set(actionByBuild.actionName, actionByBuild.action); | ||
}); | ||
} | ||
|
||
/** | ||
* Build a Proxy object to call actions in a more convenient way. e.g `.connect()` instead of `.run(connect)` | ||
*/ | ||
#buildApi(configs: NamespaceConfig): ProxiedNamespace<T> { | ||
this.#addActionsFromActionBuilders(); | ||
const namespace = new Namespace<T>(this.#id, this.#providerId, { | ||
configs, | ||
actions: this.#actions, | ||
}); | ||
this.#addHooksFromActionBuilders(namespace); | ||
|
||
const api = new Proxy(namespace, { | ||
get: (_, property) => { | ||
if (typeof property !== 'string') { | ||
throw new Error( | ||
'You can use string as your property on Namespace instance.' | ||
); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore-next-line | ||
const targetValue = namespace[property]; | ||
|
||
if ( | ||
allowedMethods.includes(property as (typeof allowedMethods)[number]) | ||
) { | ||
return targetValue.bind(namespace); | ||
} | ||
|
||
/* | ||
* This is useful accessing values like `version`, If we don't do this, we should whitelist | ||
* All the values as well, So it can be confusing for someone that only wants to add a public value to `Namespace` | ||
*/ | ||
const allowedPublicValues = ['string', 'number']; | ||
if (allowedPublicValues.includes(typeof targetValue)) { | ||
return targetValue; | ||
} | ||
|
||
return namespace.run.bind(namespace, property as keyof T); | ||
}, | ||
set: () => { | ||
throw new Error('You can not set anything on this object.'); | ||
}, | ||
}); | ||
|
||
return api as unknown as ProxiedNamespace<T>; | ||
} | ||
} |
Oops, something went wrong.