Skip to content

Commit

Permalink
feat: introducing hub, our new wallet management
Browse files Browse the repository at this point in the history
  • Loading branch information
yeager-eren committed Aug 15, 2024
1 parent 2928c8d commit 59f19eb
Show file tree
Hide file tree
Showing 58 changed files with 2,941 additions and 3 deletions.
30 changes: 27 additions & 3 deletions wallets/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,50 @@
"./legacy": {
"types": "./dist/legacy/mod.d.ts",
"default": "./dist/legacy/mod.js"
},
"./utils": {
"types": "./dist/utils/mod.d.ts",
"default": "./dist/utils/mod.js"
},
"./namespaces/common": {
"types": "./dist/namespaces/common/mod.d.ts",
"default": "./dist/namespaces/common/mod.js"
},
"./namespaces/evm": {
"types": "./dist/namespaces/evm/mod.d.ts",
"default": "./dist/namespaces/evm/mod.js"
},
"./namespaces/solana": {
"types": "./dist/namespaces/solana/mod.d.ts",
"default": "./dist/namespaces/solana/mod.js"
}
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "node ../../scripts/build/command.mjs --path wallets/core --inputs src/mod.ts,src/legacy/mod.ts",
"build": "node ../../scripts/build/command.mjs --path wallets/core --inputs src/mod.ts,src/utils/mod.ts,src/legacy/mod.ts,src/namespaces/evm/mod.ts,src/namespaces/solana/mod.ts,src/namespaces/common/mod.ts",
"ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json",
"clean": "rimraf dist",
"format": "prettier --write '{.,src}/**/*.{ts,tsx}'",
"lint": "eslint \"**/*.{ts,tsx}\" --ignore-path ../../.eslintignore"
"lint": "eslint \"**/*.{ts,tsx}\" --ignore-path ../../.eslintignore",
"test": "vitest",
"coverage": "vitest run --coverage"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"rango-types": "^0.1.69"
"rango-types": "^0.1.69",
"zustand": "^4.5.2",
"immer": "^10.0.4",
"caip": "^1.1.1"
},
"devDependencies": {
"viem": "^2.9.8"
},
"publishConfig": {
"access": "public"
Expand Down
89 changes: 89 additions & 0 deletions wallets/core/src/builders/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { Actions, Context, HookActions } from '../hub/namespaces/types.js';
import type {
AnyFunction,
FunctionWithContext,
} from '../namespaces/common/types.js';

export interface ActionByBuilder<T, Context> {
actionName: keyof T;
and: HookActions<T>;
or: HookActions<T>;
after: HookActions<T>;
before: HookActions<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: HookActions<T> = new Map();
#or: HookActions<T> = new Map();
#after: HookActions<T> = new Map();
#before: HookActions<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,
};
}
}
5 changes: 5 additions & 0 deletions wallets/core/src/builders/mod.ts
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';
229 changes: 229 additions & 0 deletions wallets/core/src/builders/namespace.ts
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 '../namespaces/common/types.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',
'or',
'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(actionByBuild.actionName, action);
});
});

actionByBuild.or.forEach((orHooks) => {
orHooks.map((action) => {
namespace.or(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>;
}
}
Loading

0 comments on commit 59f19eb

Please sign in to comment.