From 8d12a47d75dad06dc5ad2dd5309a25987848ea09 Mon Sep 17 00:00:00 2001 From: Tomek Marciniak Date: Sat, 2 Mar 2024 21:59:53 +0100 Subject: [PATCH 1/8] feat(gridplus): add onboarding views --- .prettierignore | 2 +- background/main.ts | 58 +++- background/package.json | 1 + background/redux-slices/gridplus.ts | 76 +++++ background/redux-slices/index.ts | 2 + background/services/gridplus/index.ts | 98 ++++++ background/services/index.ts | 1 + dev-utils/extension-reload.js | 124 +++---- package.json | 4 + ui/_locales/en/messages.json | 3 +- ui/hooks/dom-hooks.ts | 5 +- .../Onboarding/Tabbed/AddWalletOptions.tsx | 6 + .../Onboarding/Tabbed/GridPlus/GridPlus.tsx | 64 ++++ .../Tabbed/GridPlus/GridPlusCredentials.tsx | 52 +++ .../GridPlus/GridPlusImportAddresses.tsx | 59 ++++ .../Tabbed/GridPlus/GridPlusPairingCode.tsx | 44 +++ ui/pages/Onboarding/Tabbed/Root.tsx | 4 + ui/pages/Onboarding/Tabbed/Routes.ts | 1 + ui/public/images/add_wallet/gridplus.svg | 4 + ui/utils/gridplusHooks.tsx | 10 + webpack.config.ts | 4 + yarn.lock | 319 +++++++++++++++++- 22 files changed, 861 insertions(+), 80 deletions(-) create mode 100644 background/redux-slices/gridplus.ts create mode 100644 background/services/gridplus/index.ts create mode 100644 ui/pages/Onboarding/Tabbed/GridPlus/GridPlus.tsx create mode 100644 ui/pages/Onboarding/Tabbed/GridPlus/GridPlusCredentials.tsx create mode 100644 ui/pages/Onboarding/Tabbed/GridPlus/GridPlusImportAddresses.tsx create mode 100644 ui/pages/Onboarding/Tabbed/GridPlus/GridPlusPairingCode.tsx create mode 100644 ui/public/images/add_wallet/gridplus.svg create mode 100644 ui/utils/gridplusHooks.tsx diff --git a/.prettierignore b/.prettierignore index 6a98a61986..55857daa65 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,4 @@ ui/_locales/**/*.json !.github ci/cache .vscode -size-plugin.json \ No newline at end of file +size-plugin.json diff --git a/background/main.ts b/background/main.ts index a9e1df4c50..fea000928a 100644 --- a/background/main.ts +++ b/background/main.ts @@ -31,6 +31,7 @@ import { ServiceCreatorFunction, IslandService, LedgerService, + GridplusService, SigningService, NFTsService, WalletConnectService, @@ -200,6 +201,8 @@ import { makeFlashbotsProviderCreator } from "./services/chain/serial-fallback-p import { AnalyticsPreferences, DismissableItem } from "./services/preferences" import { newPricePoints } from "./redux-slices/prices" import NotificationsService from "./services/notifications" +import { resetGridPlusState } from "./redux-slices/gridplus" +import { GridPlusAddress } from "./services/gridplus" // This sanitizer runs on store and action data before serializing for remote // redux devtools. The goal is to end up with an object that is directly @@ -219,7 +222,7 @@ const devToolsSanitizer = (input: unknown) => { } } -const persistStoreFn = (state: T) => { +const persistStoreFn = (state: T) => { if (process.env.WRITE_REDUX_CACHE === "true") { // Browser extension storage supports JSON natively, despite that we have // to stringify to preserve BigInts @@ -337,6 +340,8 @@ export default class Main extends BaseService { const ledgerService = LedgerService.create() + const gridplusService = GridplusService.create() + const signingService = SigningService.create( internalSignerService, ledgerService, @@ -406,6 +411,7 @@ export default class Main extends BaseService { await islandService, await telemetryService, await ledgerService, + await gridplusService, await signingService, await analyticsService, await nftsService, @@ -477,6 +483,11 @@ export default class Main extends BaseService { */ private ledgerService: LedgerService, + /** + * A promise to the GridPlus service, handling the communication + */ + private gridplusService: GridplusService, + /** * A promise to the signing service which will route operations between the UI * and the exact signing services. @@ -615,6 +626,7 @@ export default class Main extends BaseService { this.islandService.startService(), this.telemetryService.startService(), this.ledgerService.startService(), + this.gridplusService.startService(), this.signingService.startService(), this.analyticsService.startService(), this.nftsService.startService(), @@ -639,6 +651,7 @@ export default class Main extends BaseService { this.islandService.stopService(), this.telemetryService.stopService(), this.ledgerService.stopService(), + this.gridplusService.stopService(), this.signingService.stopService(), this.analyticsService.stopService(), this.nftsService.stopService(), @@ -662,6 +675,7 @@ export default class Main extends BaseService { this.connectIslandService() this.connectTelemetryService() this.connectLedgerService() + this.connectGridplusService() this.connectSigningService() this.connectAnalyticsService() this.connectWalletConnectService() @@ -790,6 +804,38 @@ export default class Main extends BaseService { return this.ledgerService.refreshConnectedLedger() } + async connectGridplus({ + deviceId, + password, + }: { + deviceId?: string + password?: string + }) { + return this.gridplusService.setupClient({ deviceId, password }) + } + + async pairGridplusDevice({ pairingCode }: { pairingCode: string }) { + return this.gridplusService.pairDevice({ pairingCode }) + } + + async fetchGridPlusAddresses({ + n = 10, + startPath = [0x80000000 + 44, 0x80000000 + 60, 0x80000000, 0, 0], + }: { + n?: number + startPath?: number[] + }) { + return this.gridplusService.fetchAddresses({ n, startPath }) + } + + async importGridPlusAddresses({ + addresses, + }: { + addresses: GridPlusAddress[] + }) { + return this.gridplusService.importAddresses({ addresses }) + } + async getAccountEthBalanceUncached( addressNetwork: AddressOnNetwork, ): Promise { @@ -1197,6 +1243,10 @@ export default class Main extends BaseService { }) } + async connectGridplusService(): Promise { + this.store.dispatch(resetGridPlusState()) + } + async connectInternalSignerService(): Promise { this.internalSignerService.emitter.on("internalSigners", (signers) => { this.store.dispatch(updateInternalSigners(signers)) @@ -1825,9 +1875,9 @@ export default class Main extends BaseService { AnalyticsEvent.NEW_ACCOUNT_TO_TRACK, { description: ` - This event is fired when any address on a network is added to the tracked list. - - Note: this does not track recovery phrase(ish) import! But when an address is used + This event is fired when any address on a network is added to the tracked list. + + Note: this does not track recovery phrase(ish) import! But when an address is used on a network for the first time (read-only or recovery phrase/ledger/keyring/private key). `, }, diff --git a/background/package.json b/background/package.json index 1bcd961a75..aee4a6028c 100644 --- a/background/package.json +++ b/background/package.json @@ -58,6 +58,7 @@ "dexie": "^3.0.4", "emittery": "^0.9.2", "ethers": "5.7.2", + "gridplus-sdk": "^2.5.2", "immer": "^9.0.1", "jsondiffpatch": "^0.4.1", "lodash": "^4.17.21", diff --git a/background/redux-slices/gridplus.ts b/background/redux-slices/gridplus.ts new file mode 100644 index 0000000000..db3dd5e8b5 --- /dev/null +++ b/background/redux-slices/gridplus.ts @@ -0,0 +1,76 @@ +import { createSlice } from "@reduxjs/toolkit" +import { createBackgroundAsyncThunk } from "./utils" +import { type GridPlusAddress } from "../services/gridplus" + +export type GridPlusState = { + importableAddresses: string[] +} + +export const initialState: GridPlusState = { + importableAddresses: [], +} + +const gridplusSlice = createSlice({ + name: "gridplus", + initialState, + reducers: { + resetGridPlusState: (immerState) => { + immerState.importableAddresses = [] + }, + setImportableAddresses: ( + immerState, + { payload: importableAddresses }: { payload: string[] }, + ) => { + immerState.importableAddresses = importableAddresses + }, + }, +}) + +export const { resetGridPlusState, setImportableAddresses } = + gridplusSlice.actions + +export default gridplusSlice.reducer + +export const connectGridplus = createBackgroundAsyncThunk( + "gridplus/connect", + async ( + { deviceId, password }: { deviceId?: string; password?: string }, + { extra: { main } }, + ) => { + return main.connectGridplus({ deviceId, password }) + }, +) + +export const pairGridplusDevice = createBackgroundAsyncThunk( + "gridplus/pairDevice", + async ({ pairingCode }: { pairingCode: string }, { extra: { main } }) => { + return main.pairGridplusDevice({ pairingCode }) + }, +) + +export const fetchGridPlusAddresses = createBackgroundAsyncThunk( + "gridplus/fetchAddresses", + async ( + { + n = 10, + startPath = [0x80000000 + 44, 0x80000000 + 60, 0x80000000, 0, 0], + }: { n?: number; startPath?: number[] }, + { dispatch, extra: { main } }, + ) => { + return dispatch( + setImportableAddresses( + await main.fetchGridPlusAddresses({ n, startPath }), + ), + ) + }, +) + +export const importGridPlusAddresses = createBackgroundAsyncThunk( + "gridplus/importAddresses", + async ( + { addresses }: { addresses: GridPlusAddress[] }, + { extra: { main } }, + ) => { + return main.importGridPlusAddresses({ addresses }) + }, +) diff --git a/background/redux-slices/index.ts b/background/redux-slices/index.ts index 0f5a9a5ee4..e63b06efdf 100644 --- a/background/redux-slices/index.ts +++ b/background/redux-slices/index.ts @@ -16,6 +16,7 @@ import signingReducer from "./signing" import earnReducer from "./earn" import nftsReducer from "./nfts" import pricesReducer from "./prices" +import gridplusReducer from "./gridplus" const mainReducer = combineReducers({ account: accountsReducer, @@ -34,6 +35,7 @@ const mainReducer = combineReducers({ abilities: abilitiesReducer, nfts: nftsReducer, prices: pricesReducer, + gridplus: gridplusReducer, }) export default mainReducer diff --git a/background/services/gridplus/index.ts b/background/services/gridplus/index.ts new file mode 100644 index 0000000000..976faef57b --- /dev/null +++ b/background/services/gridplus/index.ts @@ -0,0 +1,98 @@ +import BaseService from "../base" +import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" +import { storage } from "webextension-polyfill" +import { fetchAddresses, pair, setup } from "gridplus-sdk" + +const APP_NAME = "Taho Wallet" +const CLIENT_STORAGE_KEY = "GRIDPLUS_CLIENT" +const ADDRESSES_STORAGE_KEY = "GRIDPLUS_ADDRESSES" + +export type GridPlusAddress = { + address: string + addressIndex: number +} + +type GridplusClient = string | null + +interface Events extends ServiceLifecycleEvents { + placeHolderEventForTypingPurposes: string +} + +export default class GridplusService extends BaseService { + activeAddresses: GridPlusAddress[] = [] + client: GridplusClient = null + + private constructor() { + super() + this.readClient() + this.readAddresses() + } + + static create: ServiceCreatorFunction = + async () => new this() + + async readClient() { + this.client = + (await storage.local.get(CLIENT_STORAGE_KEY))?.[CLIENT_STORAGE_KEY] ?? + null + return this.client + } + + async writeClient(client: GridplusClient) { + return storage.local.set({ + [CLIENT_STORAGE_KEY]: client, + }) + } + + async readAddresses() { + this.activeAddresses = JSON.parse( + (await storage.local.get(ADDRESSES_STORAGE_KEY))?.[ADDRESSES_STORAGE_KEY], + ) + return this.activeAddresses + } + + async writeAddresses(addresses: GridPlusAddress[]) { + return storage.local.set({ + [ADDRESSES_STORAGE_KEY]: JSON.stringify(addresses), + }) + } + + async setupClient({ + deviceId, + password, + }: { + deviceId?: string + password?: string + }) { + return setup({ + deviceId, + password, + name: APP_NAME, + getStoredClient: () => this.client ?? "", + setStoredClient: this.writeClient, + }) + } + + async pairDevice({ pairingCode }: { pairingCode: string }) { + await this.readClient() + return pair(pairingCode) + } + + async fetchAddresses({ + n = 10, + startPath = [0x80000000 + 44, 0x80000000 + 60, 0x80000000, 0, 0], + }: { + n?: number + startPath?: number[] + }) { + await this.readClient() + return fetchAddresses({ n, startPath }) + } + + async importAddresses({ addresses }: { addresses: GridPlusAddress[] }) { + addresses.forEach((address) => { + this.activeAddresses.push(address) + }) + await this.writeAddresses(this.activeAddresses) + } +} diff --git a/background/services/index.ts b/background/services/index.ts index 8572a12285..1bc648d532 100644 --- a/background/services/index.ts +++ b/background/services/index.ts @@ -18,6 +18,7 @@ export { default as InternalEthereumProviderService } from "./internal-ethereum- export { default as IslandService } from "./island" export { default as TelemetryService } from "./telemetry" export { default as LedgerService } from "./ledger" +export { default as GridplusService } from "./gridplus" export { default as SigningService } from "./signing" export { default as AnalyticsService } from "./analytics" export { default as NFTsService } from "./nfts" diff --git a/dev-utils/extension-reload.js b/dev-utils/extension-reload.js index c3875f2b47..9ac88c4490 100644 --- a/dev-utils/extension-reload.js +++ b/dev-utils/extension-reload.js @@ -53,7 +53,7 @@ window.LiveReloadOptions = { host: "localhost" } e, t, n, - r + r, ) } return n[o].exports @@ -122,7 +122,7 @@ window.LiveReloadOptions = { host: "localhost" } _this._disconnectionReason = "handshake-timeout" return _this.socket.close() } - })(this) + })(this), ) this._reconnectTimer = new Timer( (function (_this) { @@ -132,7 +132,7 @@ window.LiveReloadOptions = { host: "localhost" } } return _this.connect() } - })(this) + })(this), ) this.connect() } @@ -193,7 +193,7 @@ window.LiveReloadOptions = { host: "localhost" } this._reconnectTimer.start(this._nextDelay) return (this._nextDelay = Math.min( this.options.maxdelay, - this._nextDelay * 2 + this._nextDelay * 2, )) } } @@ -235,7 +235,7 @@ window.LiveReloadOptions = { host: "localhost" } } this._sendCommand(hello) return this._handshakeTimeout.start( - this.options.handshake_timeout + this.options.handshake_timeout, ) } @@ -243,7 +243,7 @@ window.LiveReloadOptions = { host: "localhost" } this.protocol = 0 this.handlers.disconnected( this._disconnectionReason, - this._nextDelay + this._nextDelay, ) return this._scheduleReconnection() } @@ -256,7 +256,7 @@ window.LiveReloadOptions = { host: "localhost" } return Connector })() - }.call(this)) + }).call(this) }, { "./protocol": 6 }, ], @@ -278,11 +278,11 @@ window.LiveReloadOptions = { host: "localhost" } if (event.propertyName === eventName) { return handler() } - } + }, ) } throw new Error( - `Attempt to attach custom event ${eventName} to something which isn't a DOMElement` + `Attempt to attach custom event ${eventName} to something which isn't a DOMElement`, ) }, fire(element, eventName) { @@ -298,7 +298,7 @@ window.LiveReloadOptions = { host: "localhost" } } } else { throw new Error( - `Attempt to fire custom event ${eventName} on something which isn't a DOMElement` + `Attempt to fire custom event ${eventName} on something which isn't a DOMElement`, ) } }, @@ -307,7 +307,7 @@ window.LiveReloadOptions = { host: "localhost" } exports.bind = CustomEvents.bind exports.fire = CustomEvents.fire - }.call(this)) + }).call(this) }, {}, ], @@ -370,7 +370,7 @@ window.LiveReloadOptions = { host: "localhost" } link.href = this.host.generateCacheBustUrl(link.href) } this.host.console.log( - "LiveReload is asking LESS to recompile all stylesheets" + "LiveReload is asking LESS to recompile all stylesheets", ) this.window.less.refresh(true) return true @@ -384,7 +384,7 @@ window.LiveReloadOptions = { host: "localhost" } return LessPlugin })() - }.call(this)) + }).call(this) }, {}, ], @@ -434,7 +434,7 @@ window.LiveReloadOptions = { host: "localhost" } this.window.WebSocket || this.window.MozWebSocket) ) { console.error( - "LiveReload disabled because the browser does not seem to support web sockets" + "LiveReload disabled because the browser does not seem to support web sockets", ) return } @@ -450,7 +450,7 @@ window.LiveReloadOptions = { host: "localhost" } this.options = Options.extract(this.window.document) if (!this.options) { console.error( - "LiveReload disabled because it could not find its own