Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introducing hub, our new wallet management #826

Merged
merged 1 commit into from
Sep 18, 2024
Merged
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"@vercel/routing-utils": "^3.1.0",
"@vitest/coverage-v8": "^0.33.0",
"@vitest/coverage-v8": "^2.0.5",
"buffer": "^5.5.0",
"chalk": "^5.2.0",
"command-line-args": "^5.2.1",
Expand Down Expand Up @@ -113,7 +113,7 @@
"stream-browserify": "^3.0.0",
"tty-browserify": "^0.0.1",
"util": "^0.12.3",
"vitest": "^0.33.0",
"vitest": "^2.0.5",
"vm-browserify": "^1.1.2"
},
"resolutions": {
Expand Down
27 changes: 24 additions & 3 deletions wallets/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@
"./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": [
Expand All @@ -22,19 +38,24 @@
"legacy"
],
"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"
"caip": "^1.1.1",
"immer": "^10.0.4",
"rango-types": "^0.1.69",
"zustand": "^4.5.2"
},
"publishConfig": {
"access": "public"
Expand Down
86 changes: 86 additions & 0 deletions wallets/core/src/builders/action.ts
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;
}
Ikari-Shinji-re marked this conversation as resolved.
Show resolved Hide resolved

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 '../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>;
}
}
Loading