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: split business logic into services #44

Merged
merged 15 commits into from
Feb 2, 2023
7 changes: 6 additions & 1 deletion packages/extension-chrome/src/rpc/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import browser from 'webextension-polyfill';
import { errors } from '@nexus-wallet/utils';
import { RpcMethods, ServerParams } from './types';
import { JSONRPCServer } from 'json-rpc-2.0';
import { createServicesFactory } from '../services';

export const server = new JSONRPCServer<ServerParams>();

Expand All @@ -13,9 +14,11 @@ export function addMethod<K extends keyof RpcMethods>(
server.addMethod(method, handler);
}

let servicesFactory = createServicesFactory();

export function createRpcServerParams(payload: { endpoint: Endpoint }): ServerParams {
return {
async getRequesterAppInfo() {
getRequesterAppInfo: async () => {
const tab = await browser.tabs.get(payload.endpoint.tabId);
if (!tab.url || !tab.favIconUrl) {
errors.throwError(
Expand All @@ -24,5 +27,7 @@ export function createRpcServerParams(payload: { endpoint: Endpoint }): ServerPa
}
return { url: tab.url, favIconUrl: tab.favIconUrl };
},

resolveService: (k) => servicesFactory.get(k),
};
}
7 changes: 7 additions & 0 deletions packages/extension-chrome/src/rpc/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { Script } from '@ckb-lumos/lumos';
import { Promisable } from '@nexus-wallet/types/lib/base';
import { Services } from '../services';

interface RpcCall<Params, Result> {
params: Params;
result: Result;
Expand All @@ -7,11 +11,14 @@ export interface RpcMethods {
wallet_enable: RpcCall<void, void>;
wallet_isEnabled: RpcCall<void, boolean>;
wallet_getNetworkName: RpcCall<void, string>;

wallet_fullOwnership_getUnusedLocks: RpcCall<void, Script[]>;
}

/**
* the RPC server handler second params
*/
export interface ServerParams {
resolveService<K extends keyof Services>(name: K): Promisable<Services[K]>;
getRequesterAppInfo(): Promise<{ url: string; favIconUrl: string }>;
}
71 changes: 25 additions & 46 deletions packages/extension-chrome/src/rpc/walletImpl.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,35 @@
import { addMethod } from './server';
import browser from 'webextension-polyfill';
import { errors } from '@nexus-wallet/utils';

const NOTIFICATION_WIDTH = 360;
const NOTIFICATION_HEIGHT = 600;
addMethod('wallet_enable', async (_, { getRequesterAppInfo, resolveService }) => {
const grantService = await resolveService('grantService');
const { url } = await getRequesterAppInfo();

addMethod('wallet_enable', async (_, { getRequesterAppInfo }) => {
const lastFocused = await browser.windows.getLastFocused();
const { host } = new URL(url);

const notification = await browser.windows.create({
type: 'popup',
focused: true,
top: lastFocused.top,
left: lastFocused.left! + (lastFocused.width! - 360),
width: NOTIFICATION_WIDTH,
height: NOTIFICATION_HEIGHT,
url: 'notification.html',
});
const isGranted = await grantService.getIsGranted({ host });
if (isGranted) return;

const notificationTabId = notification.tabs?.[0]?.id;
const granted = await grantService.getIsGranted({ host });
if (granted) return;

type MessageListener = Parameters<typeof browser.runtime.onMessage.addListener>[0];
const getRequesterAppInfoListener: MessageListener = (message, sender) =>
new Promise(async (sendResponse) => {
if (notificationTabId !== sender.tab?.id) return;
if (message.method !== 'getRequesterAppInfo') return;
const notificationService = await resolveService('notificationService');
try {
await notificationService.requestGrant({ url });
} catch {
errors.throwError('User has rejected');
}

const { url } = await getRequesterAppInfo();
sendResponse({ url });
browser.runtime.onMessage.removeListener(getRequesterAppInfoListener);
});

return new Promise((resolve, reject) => {
const userHasEnabledWalletListener: MessageListener = (message, sender) =>
new Promise((sendResponse) => {
if (notificationTabId !== sender.tab?.id) return;
if (message.method !== 'userHasEnabledWallet') return;

sendResponse(void 0);
browser.runtime.onMessage.removeListener(userHasEnabledWalletListener);
resolve();
});

browser.runtime.onMessage.addListener(getRequesterAppInfoListener);
browser.runtime.onMessage.addListener(userHasEnabledWalletListener);
await grantService.grant({ host });
});

browser.windows.onRemoved.addListener((windowId) => {
if (windowId === notification.id) {
browser.runtime.onMessage.removeListener(getRequesterAppInfoListener);
browser.runtime.onMessage.removeListener(userHasEnabledWalletListener);
reject(errors);
}
});
});
addMethod('wallet_fullOwnership_getUnusedLocks', () => {
// TODO implement me, this is just a mock
return [
{
codeHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
hashType: 'type',
args: '0x0000000000000000000000000000000000000000',
},
];
});
30 changes: 30 additions & 0 deletions packages/extension-chrome/src/services/grant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GrantService, Storage } from '@nexus-wallet/types';
import { errors } from '@nexus-wallet/utils';

export function createGrantService(payload: { storage: Storage<{ grant: string[] }> }): GrantService {
const { storage } = payload;

return {
async getIsGranted(payload) {
const grantedUrls = await storage.getItem('grant');
if (!grantedUrls) return false;
return grantedUrls.includes(payload.host);
},
async grant(payload) {
const grantedUrls = await storage.getItem('grant');
if (!grantedUrls) {
errors.throwError('Storage is not initialized');
}
grantedUrls.push(payload.host);
await storage.setItem('grant', grantedUrls);
},
async revoke(payload) {
const grantedUrls = await storage.getItem('grant');

if (!grantedUrls) return;

const revoked = grantedUrls.filter((host) => host === payload.host);
await storage.setItem('grant', revoked);
},
};
}
42 changes: 42 additions & 0 deletions packages/extension-chrome/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { GrantService, NotificationService, Promisable, Storage } from '@nexus-wallet/types';
import { createGrantService } from './grant';
import { createInMemoryStorage } from './storage';
import { createNotificationService } from './notification';

interface Schema {
grant: string[];
}

export interface Services {
storage: Storage<Schema>;
grantService: GrantService;
notificationService: NotificationService;
}

export interface ServicesFactory {
get<K extends keyof Services>(name: K): Promisable<Services[K]>;
}

export function createServicesFactory(): ServicesFactory {
const storage = createInMemoryStorage<Schema>();

const defaultStorage = {
grant: [],
};
storage.setAll({
homura marked this conversation as resolved.
Show resolved Hide resolved
...defaultStorage,
...storage.getAll(),
});

const services = {
storage,
grantService: createGrantService({ storage }),
notificationService: createNotificationService(),
};

return {
get(key) {
return services[key];
},
};
}
67 changes: 67 additions & 0 deletions packages/extension-chrome/src/services/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { NotificationService } from '@nexus-wallet/types';
import { errors } from '@nexus-wallet/utils';
import browser from 'webextension-polyfill';

const NOTIFICATION_WIDTH = 360;
const NOTIFICATION_HEIGHT = 600;

// TODO this is a mocked notification service,
// just demonstrating how we organize the code
export function createNotificationService(): NotificationService {
homura marked this conversation as resolved.
Show resolved Hide resolved
return {
async requestGrant({ url }) {
const lastFocused = await browser.windows.getLastFocused();

const notification = await browser.windows.create({
type: 'popup',
focused: true,
top: lastFocused.top,
left: lastFocused.left! + (lastFocused.width! - 360),
width: NOTIFICATION_WIDTH,
height: NOTIFICATION_HEIGHT,
url: 'notification.html',
});

const notificationTabId = notification.tabs?.[0]?.id;

type MessageListener = Parameters<typeof browser.runtime.onMessage.addListener>[0];
const getRequesterAppInfoListener: MessageListener = (message, sender) =>
new Promise(async (sendResponse) => {
if (notificationTabId !== sender.tab?.id) return;
if (message.method !== 'getRequesterAppInfo') return;

sendResponse({ url });
browser.runtime.onMessage.removeListener(getRequesterAppInfoListener);
});

return new Promise((resolve, reject) => {
const userHasEnabledWalletListener: MessageListener = (message, sender) =>
new Promise((sendResponse) => {
if (notificationTabId !== sender.tab?.id) return;
if (message.method !== 'userHasEnabledWallet') return;

sendResponse(void 0);
browser.runtime.onMessage.removeListener(userHasEnabledWalletListener);
resolve();
});

browser.runtime.onMessage.addListener(getRequesterAppInfoListener);
browser.runtime.onMessage.addListener(userHasEnabledWalletListener);

browser.windows.onRemoved.addListener((windowId) => {
if (windowId === notification.id) {
browser.runtime.onMessage.removeListener(getRequesterAppInfoListener);
browser.runtime.onMessage.removeListener(userHasEnabledWalletListener);
reject(errors);
}
});
});
},
requestSignTransaction() {
errors.unimplemented();
},
requestSignData() {
errors.unimplemented();
},
};
}
37 changes: 37 additions & 0 deletions packages/extension-chrome/src/services/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Storage } from '@nexus-wallet/types';
import { errors } from '@nexus-wallet/utils';

interface InMemoryStorage<S> extends Storage<S> {
getAll(): S | undefined;

setAll(s: S): void;
}

export function createInMemoryStorage<S>(): InMemoryStorage<S> {
const store = new Map();

return {
getItem(key) {
return store.get(key);
},
hasItem(key) {
return store.has(key);
},
removeItem(key) {
return store.delete(key);
},
setItem(key, value) {
store.set(key, value);
},
getAll() {
return Object.fromEntries(store.entries());
},
setAll(s) {
if (!s) errors.throwError(`The storage cannot be set to ${s}`);

Object.entries(s).forEach(([key, value]) => {
store.set(key, value);
});
},
};
}
3 changes: 3 additions & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export type { InjectedCkb, CkbProvider } from './injected';
export type { OwnershipService, NotificationService, KeystoreService, GrantService } from './services';
export type { Storage } from './storage';
export type { Promisable, Paginate, Cursor } from './base';
Loading