From c7e899f408bccdabf8c77b21a2b250005a96f7af Mon Sep 17 00:00:00 2001 From: huanniangstudio <891863891@qq.com> Date: Sat, 4 Mar 2023 11:41:45 +0800 Subject: [PATCH] Update background controllers --- src/background/controller/base.ts | 45 +++-- src/background/controller/index.ts | 1 + .../controller/provider/controller.ts | 114 +++++++++++ src/background/controller/provider/index.ts | 29 +++ .../controller/provider/internalMethod.ts | 32 ++++ src/background/controller/provider/rpcFlow.ts | 179 ++++++++++++++++++ src/background/controller/wallet.ts | 89 ++++++++- src/background/index.ts | 28 ++- tsconfig.json | 3 +- 9 files changed, 495 insertions(+), 25 deletions(-) create mode 100644 src/background/controller/provider/controller.ts create mode 100644 src/background/controller/provider/index.ts create mode 100644 src/background/controller/provider/internalMethod.ts create mode 100644 src/background/controller/provider/rpcFlow.ts diff --git a/src/background/controller/base.ts b/src/background/controller/base.ts index a6d35510..557b9688 100644 --- a/src/background/controller/base.ts +++ b/src/background/controller/base.ts @@ -1,24 +1,29 @@ +import { cloneDeep } from 'lodash'; + +import { keyringService, preferenceService } from '../service'; +import { Account } from '../service/preference'; + class BaseController { - // getCurrentAccount = async () => { - // let account = preferenceService.getCurrentAccount() - // if (account) { - // const accounts = await this.getAccounts() - // const matchAcct = accounts.find((acct) => account!.address === acct.address) - // if (!matchAcct) account = undefined - // } - // if (!account) { - // ;[account] = await this.getAccounts() - // if (!account) return null - // preferenceService.setCurrentAccount(account) - // } - // return cloneDeep(account) as Account - // } - // syncGetCurrentAccount = () => { - // return preferenceService.getCurrentAccount() || null - // } - // getAccounts = (): Promise => { - // return keyringService.getAllVisibleAccountsArray() - // } + getCurrentAccount = async () => { + let account = preferenceService.getCurrentAccount(); + if (account) { + const accounts = await this.getAccounts(); + const matchAcct = accounts.find((acct) => account!.address === acct.address); + if (!matchAcct) account = undefined; + } + if (!account) { + [account] = await this.getAccounts(); + if (!account) return null; + preferenceService.setCurrentAccount(account); + } + return cloneDeep(account) as Account; + }; + syncGetCurrentAccount = () => { + return preferenceService.getCurrentAccount() || null; + }; + getAccounts = (): Promise => { + return keyringService.getAllVisibleAccountsArray(); + }; } export default BaseController; diff --git a/src/background/controller/index.ts b/src/background/controller/index.ts index 62009f55..f304c1ad 100644 --- a/src/background/controller/index.ts +++ b/src/background/controller/index.ts @@ -1 +1,2 @@ export { default as walletController } from './wallet'; +export { default as providerController } from './provider'; diff --git a/src/background/controller/provider/controller.ts b/src/background/controller/provider/controller.ts new file mode 100644 index 00000000..91132a0c --- /dev/null +++ b/src/background/controller/provider/controller.ts @@ -0,0 +1,114 @@ + +import { permissionService, sessionService } from '@/background/service'; +import { CHAINS } from '@/shared/constant'; + +import BaseController from '../base'; +import wallet from '../wallet'; +import { publicKeyToAddress } from '@/background/utils/tx-utils'; + + + +class ProviderController extends BaseController { + + connect = async ({ session: { origin } }) => { + console.log('hasPermiss',origin,permissionService.hasPermission(origin)) + if (!permissionService.hasPermission(origin)) { + // throw ethErrors.provider.unauthorized(); + } + + const _account = await this.getCurrentAccount(); + const account = _account ? [_account.address.toLowerCase()] : []; + sessionService.broadcastEvent('accountsChanged', account); + const connectSite = permissionService.getConnectedSite(origin); + if (connectSite) { + const chain = CHAINS[connectSite.chain]; + sessionService.broadcastEvent( + 'chainChanged', + { + networkVersion: chain.network + }, + origin + ); + } + return _account + }; + + @Reflect.metadata('SAFE', true) + getNetwork = async () => { + const network = wallet.getNetworkType() + return network; + }; + + @Reflect.metadata('SAFE', true) + getAddress = async () => { + const account = await wallet.getCurrentAccount(); + if(!account) return '' + const addressType = wallet.getAddressType(); + const networkType = wallet.getNetworkType() + const address = publicKeyToAddress(account.address,addressType,networkType) + return address; + }; + + @Reflect.metadata('SAFE', true) + getPublicKey = async () => { + const account = await wallet.getCurrentAccount(); + if(!account) return '' + return account.address; + }; + + @Reflect.metadata('SAFE', true) + getBalance = async () => { + const account = await this.getCurrentAccount(); + if (!account) return null; + const balance = await wallet.getAddressBalance(account.address) + return balance; + }; + + @Reflect.metadata('APPROVAL', ['SendBitcoin', () => { + // todo check + }]) + sendBitcoin = async () => { + // todo + } + + @Reflect.metadata('APPROVAL', ['SendInscription', () => { + // todo check + }]) + sendInscription = async () => { + // todo + } + + @Reflect.metadata('APPROVAL', ['SignText', () => { + // todo check + }]) + signText = async () => { + // todo + } + + @Reflect.metadata('APPROVAL', ['SignTx', () => { + // ttodo check + }]) + signTx = async () => { + const account = await this.getCurrentAccount(); + console.log(account,'go') + } + + @Reflect.metadata('SAFE',true) + pushTx = async () => { + // todo + } + + @Reflect.metadata('APPROVAL', ['SignPsbt', () => { + // todo check + }]) + signPsbt = async () => { + // todo + } + + @Reflect.metadata('SAFE', true) + pushPsbt = async () => { + // todo + } +} + +export default new ProviderController(); diff --git a/src/background/controller/provider/index.ts b/src/background/controller/provider/index.ts new file mode 100644 index 00000000..260370e0 --- /dev/null +++ b/src/background/controller/provider/index.ts @@ -0,0 +1,29 @@ +import { ethErrors } from 'eth-rpc-errors'; + +import { sessionService, keyringService } from '@/background/service'; +import { tab } from '@/background/webapi'; + +import internalMethod from './internalMethod'; +import rpcFlow from './rpcFlow'; + +tab.on('tabRemove', (id) => { + sessionService.deleteSession(id); +}); + +export default async (req) => { + const { + data: { method } + } = req; + + if (internalMethod[method]) { + return internalMethod[method](req); + } + + const hasVault = keyringService.hasVault(); + if (!hasVault) { + throw ethErrors.provider.userRejectedRequest({ + message: 'wallet must has at least one account' + }); + } + return rpcFlow(req); +}; diff --git a/src/background/controller/provider/internalMethod.ts b/src/background/controller/provider/internalMethod.ts new file mode 100644 index 00000000..b2bb827f --- /dev/null +++ b/src/background/controller/provider/internalMethod.ts @@ -0,0 +1,32 @@ +import { keyringService } from '@/background/service'; + +import providerController from './controller'; + +const tabCheckin = ({ + data: { + params: { origin, name, icon } + }, + session +}) => { + session.setProp({ origin, name, icon }); +}; + +const getProviderState = async (req) => { + const { + session: { origin } + } = req; + + const isUnlocked = keyringService.memStore.getState().isUnlocked; + + return { + network: 'mainnet', + isUnlocked, + accounts: isUnlocked ? await providerController.getAddress() : [], + networkVersion: '' + }; +}; + +export default { + tabCheckin, + getProviderState +}; diff --git a/src/background/controller/provider/rpcFlow.ts b/src/background/controller/provider/rpcFlow.ts new file mode 100644 index 00000000..146cc29d --- /dev/null +++ b/src/background/controller/provider/rpcFlow.ts @@ -0,0 +1,179 @@ +import { ethErrors } from 'eth-rpc-errors'; +import 'reflect-metadata'; + +import { keyringService, notificationService, permissionService } from '@/background/service'; +import { PromiseFlow, underline2Camelcase } from '@/background/utils'; +import { CHAINS_ENUM, EVENTS } from '@/shared/constant'; +import eventBus from '@/shared/eventBus'; + +import providerController from './controller'; + +const isSignApproval = (type: string) => { + const SIGN_APPROVALS = ['SignText', 'SignPsbt', 'SignTx']; + return SIGN_APPROVALS.includes(type); +}; + +const flow = new PromiseFlow(); +const flowContext = flow + .use(async (ctx, next) => { + // check method + const { + data: { method } + } = ctx.request; + ctx.mapMethod = underline2Camelcase(method); + + if (!providerController[ctx.mapMethod]) { + throw ethErrors.rpc.methodNotFound({ + message: `method [${method}] doesn't has corresponding handler`, + data: ctx.request.data + }); + } + + return next(); + }) + .use(async (ctx, next) => { + const { mapMethod } = ctx; + if (!Reflect.getMetadata('SAFE', providerController, mapMethod)) { + // check lock + const isUnlock = keyringService.memStore.getState().isUnlocked; + + if (!isUnlock) { + ctx.request.requestedApproval = true; + await notificationService.requestApproval({ lock: true }); + } + } + + return next(); + }) + .use(async (ctx, next) => { + // check connect + const { + request: { + session: { origin, name, icon } + }, + mapMethod + } = ctx; + if (!Reflect.getMetadata('SAFE', providerController, mapMethod)) { + if (!permissionService.hasPermission(origin)) { + ctx.request.requestedApproval = true; + await notificationService.requestApproval( + { + params: { origin, name, icon }, + approvalComponent: 'Connect' + }, + { height: 800 } + ); + permissionService.addConnectedSite(origin, name, icon, CHAINS_ENUM.BTC); + } + } + + return next(); + }) + .use(async (ctx, next) => { + // check need approval + const { + request: { + data: { params, method }, + session: { origin, name, icon } + }, + mapMethod + } = ctx; + const [approvalType, condition, options = {}] = + Reflect.getMetadata('APPROVAL', providerController, mapMethod) || []; + const windowHeight = 800; + if (approvalType && (!condition || !condition(ctx.request))) { + ctx.request.requestedApproval = true; + ctx.approvalRes = await notificationService.requestApproval( + { + approvalComponent: approvalType, + params: { + method, + data: params, + session: { origin, name, icon } + }, + origin + }, + { height: windowHeight } + ); + if (isSignApproval(approvalType)) { + permissionService.updateConnectSite(origin, { isSigned: true }, true); + } else { + permissionService.touchConnectedSite(origin); + } + } + + return next(); + }) + .use(async (ctx) => { + const { approvalRes, mapMethod, request } = ctx; + // process request + const [approvalType] = Reflect.getMetadata('APPROVAL', providerController, mapMethod) || []; + console.log('paa', approvalType, approvalRes); + const { uiRequestComponent, ...rest } = approvalRes || {}; + const { + session: { origin } + } = request; + const requestDefer = Promise.resolve( + providerController[mapMethod]({ + ...request, + approvalRes + }) + ); + + requestDefer + .then((result) => { + if (isSignApproval(approvalType)) { + eventBus.emit(EVENTS.broadcastToUI, { + method: EVENTS.SIGN_FINISHED, + params: { + success: true, + data: result + } + }); + } + return result; + }) + .catch((e: any) => { + if (isSignApproval(approvalType)) { + eventBus.emit(EVENTS.broadcastToUI, { + method: EVENTS.SIGN_FINISHED, + params: { + success: false, + errorMsg: JSON.stringify(e) + } + }); + } + }); + async function requestApprovalLoop({ uiRequestComponent, ...rest }) { + ctx.request.requestedApproval = true; + const res = await notificationService.requestApproval({ + approvalComponent: uiRequestComponent, + params: rest, + origin, + approvalType + }); + if (res.uiRequestComponent) { + return await requestApprovalLoop(res); + } else { + return res; + } + } + if (uiRequestComponent) { + ctx.request.requestedApproval = true; + return await requestApprovalLoop({ uiRequestComponent, ...rest }); + } + + return requestDefer; + }) + .callback(); + +export default (request) => { + const ctx: any = { request: { ...request, requestedApproval: false } }; + return flowContext(ctx).finally(() => { + if (ctx.request.requestedApproval) { + flow.requestedApproval = false; + // only unlock notification if current flow is an approval flow + notificationService.unLock(); + } + }); +}; diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index 68ed0928..bde0523e 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -5,11 +5,20 @@ import ECPairFactory from 'ecpair'; import { cloneDeep, groupBy } from 'lodash'; import * as ecc from 'tiny-secp256k1'; -import { contactBookService, keyringService, openapiService, preferenceService } from '@/background/service'; +import { + contactBookService, + keyringService, + notificationService, + openapiService, + permissionService, + preferenceService, + sessionService +} from '@/background/service'; import i18n from '@/background/service/i18n'; import { DisplayedKeryring, Keyring, KEYRING_CLASS, ToSignInput } from '@/background/service/keyring'; import { BRAND_ALIAN_TYPE_TEXT, + CHAINS_ENUM, COIN_NAME, COIN_SYMBOL, KEYRING_TYPE, @@ -21,6 +30,7 @@ import { Wallet } from '@unisat/bitcoinjs-wallet'; import { ContactBookItem } from '../service/contactBook'; import { OpenApiService } from '../service/openapi'; +import { ConnectedSite } from '../service/permission'; import { Account } from '../service/preference'; import { SingleAccountTransaction, toPsbtNetwork } from '../utils/tx-utils'; import BaseController from './base'; @@ -45,6 +55,10 @@ export class WalletController extends BaseController { boot = (password: string) => keyringService.boot(password); isBooted = () => keyringService.isBooted(); + getApproval = notificationService.getApproval; + resolveApproval = notificationService.resolveApproval; + rejectApproval = notificationService.rejectApproval; + hasVault = () => keyringService.hasVault(); verifyPassword = (password: string) => keyringService.verifyPassword(password); changePassword = (password: string, newPassword: string) => keyringService.changePassword(password, newPassword); @@ -99,6 +113,7 @@ export class WalletController extends BaseController { const alianNameInited = preferenceService.getInitAlianNameStatus(); const alianNames = contactBookService.listAlias(); await keyringService.submitPassword(password); + sessionService.broadcastEvent('unlock'); if (!alianNameInited && alianNames.length === 0) { this.initAlianNames(); } @@ -109,7 +124,10 @@ export class WalletController extends BaseController { lockWallet = async () => { await keyringService.setLocked(); + sessionService.broadcastEvent('accountsChanged', []); + sessionService.broadcastEvent('lock'); }; + setPopupOpen = (isOpen: boolean) => { preferenceService.setPopupOpen(isOpen); }; @@ -537,7 +555,7 @@ export class WalletController extends BaseController { preferenceService.setAddressType(addressType); }; - getNetworkType = async () => { + getNetworkType = () => { const networkType = preferenceService.getNetworkType(); return networkType; }; @@ -701,6 +719,73 @@ export class WalletController extends BaseController { const data = await openapiService.getAddressUtxo(address); return data; }; + + getConnectedSite = permissionService.getConnectedSite; + getSite = permissionService.getSite; + getConnectedSites = permissionService.getConnectedSites; + setRecentConnectedSites = (sites: ConnectedSite[]) => { + permissionService.setRecentConnectedSites(sites); + }; + getRecentConnectedSites = () => { + return permissionService.getRecentConnectedSites(); + }; + getCurrentSite = (tabId: number): ConnectedSite | null => { + const { origin, name, icon } = sessionService.getSession(tabId) || {}; + if (!origin) { + return null; + } + const site = permissionService.getSite(origin); + if (site) { + return site; + } + return { + origin, + name, + icon, + chain: CHAINS_ENUM.BTC, + isConnected: false, + isSigned: false, + isTop: false + }; + }; + getCurrentConnectedSite = (tabId: number) => { + const { origin } = sessionService.getSession(tabId) || {}; + return permissionService.getWithoutUpdate(origin); + }; + setSite = (data: ConnectedSite) => { + permissionService.setSite(data); + if (data.isConnected) { + sessionService.broadcastEvent( + 'chainChanged', + { + chain: CHAINS_ENUM.BTC, + networkVersion: 1 + }, + data.origin + ); + } + }; + updateConnectSite = (origin: string, data: ConnectedSite) => { + permissionService.updateConnectSite(origin, data); + sessionService.broadcastEvent( + 'chainChanged', + { + chain: CHAINS_ENUM.BTC, + networkVersion: 1 + }, + data.origin + ); + }; + removeAllRecentConnectedSites = () => { + const sites = permissionService.getRecentConnectedSites().filter((item) => !item.isTop); + sites.forEach((item) => { + this.removeConnectedSite(item.origin); + }); + }; + removeConnectedSite = (origin: string) => { + sessionService.broadcastEvent('accountsChanged', [], origin); + permissionService.removeConnectedSite(origin); + }; } export default new WalletController(); diff --git a/src/background/index.ts b/src/background/index.ts index d489ce97..9b32995f 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -2,13 +2,14 @@ import { EVENTS } from '@/shared/constant'; import eventBus from '@/shared/eventBus'; import { Message } from '@/shared/utils'; -import { walletController } from './controller'; -import { contactBookService, keyringService, openapiService, preferenceService } from './service'; +import { providerController, walletController } from './controller'; +import { contactBookService, keyringService, openapiService, preferenceService, sessionService } from './service'; import { storage } from './webapi'; import browser from './webapi/browser'; const { PortMessage } = Message; +let appStoreLoaded = false; async function restoreAppState() { const keyringState = await storage.get('keyringState'); keyringService.loadStore(keyringState); @@ -17,6 +18,8 @@ async function restoreAppState() { await preferenceService.init(); await contactBookService.init(); + + appStoreLoaded = true; } restoreAppState(); @@ -68,4 +71,25 @@ browser.runtime.onConnect.addListener((port) => { return; } + + const pm = new PortMessage(port); + pm.listen(async (data) => { + if (!appStoreLoaded) { + // todo + } + const sessionId = port.sender?.tab?.id; + const session = sessionService.getOrCreateSession(sessionId); + + const req = { data, session }; + // for background push to respective page + req.session.pushMessage = (event, data) => { + pm.send('message', { event, data }); + }; + + return providerController(req); + }); + + port.onDisconnect.addListener(() => { + // todo + }); }); diff --git a/tsconfig.json b/tsconfig.json index 6e08e37f..4d87ccee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "resolveJsonModule": true, "isolatedModules": false, "jsx": "react-jsx", - "plugins": [{ "transform": "typescript-transform-paths", "afterDeclarations": true }] + "plugins": [{ "transform": "typescript-transform-paths", "afterDeclarations": true }], + "experimentalDecorators": true }, "exclude": ["./node_modules"], "include": ["src"]