diff --git a/.env.defaults b/.env.defaults index 33c1a64e13..d24ef383e7 100644 --- a/.env.defaults +++ b/.env.defaults @@ -37,3 +37,4 @@ USE_MAINNET_FORK=false ARBITRUM_FORK_RPC=https://rpc.tenderly.co/fork/2fc2cf12-5c58-439f-9b5e-967bfd02191a TESTNET_TAHO_DEPLOYER_ADDRESS=0x55B180c3470dA8E31761d45468e4E61DbE13Eb9B TESTNET_TAHO_ADDRESS=0x78f04eC76df38Fcb37971Efa8EcbcB33f52dae0F +MOCKED_GRIDPLUS_ONBOARDING=false diff --git a/.github/workflows/pledge-signer-sync/package.json b/.github/workflows/pledge-signer-sync/package.json index be96d8ab34..da0080c779 100644 --- a/.github/workflows/pledge-signer-sync/package.json +++ b/.github/workflows/pledge-signer-sync/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "private": true, "engines": { - "node": ">=16.0.0 <17.0.0" + "node": ">=16.0.0 <19.0.0" }, "dependencies": { "firebase": "^9.9.0", diff --git a/.github/workflows/pledge-signer-sync/pledge-export.js b/.github/workflows/pledge-signer-sync/pledge-export.js index 5541751979..8383a1bbd6 100644 --- a/.github/workflows/pledge-signer-sync/pledge-export.js +++ b/.github/workflows/pledge-signer-sync/pledge-export.js @@ -1,4 +1,5 @@ // @ts-check +/* eslint-disable import/no-unresolved */ /* eslint-disable no-console */ // need logging /* eslint-disable no-await-in-loop */ // need to process items in sequence import { getAuth, signInWithEmailAndPassword } from "firebase/auth" diff --git a/.github/workflows/pledge-signer-sync/pledge-sync.js b/.github/workflows/pledge-signer-sync/pledge-sync.js index eea1cdab4a..ad2de7d9e2 100644 --- a/.github/workflows/pledge-signer-sync/pledge-sync.js +++ b/.github/workflows/pledge-signer-sync/pledge-sync.js @@ -1,4 +1,5 @@ // @ts-check +/* eslint-disable import/no-unresolved */ /* eslint-disable no-console */ // need logging /* eslint-disable no-await-in-loop */ // need to process items in sequence import { getAuth, signInWithEmailAndPassword } from "firebase/auth" @@ -103,16 +104,16 @@ const syncGalxe = async () => { const payload = { operationName: "credentialItems", query: ` - mutation credentialItems($credId: ID!, $operation: Operation!, $items: [String!]!) - { - credentialItems(input: { - credId: $credId - operation: $operation - items: $items - }) - { - name - } + mutation credentialItems($credId: ID!, $operation: Operation!, $items: [String!]!) + { + credentialItems(input: { + credId: $credId + operation: $operation + items: $items + }) + { + name + } } `, variables: { diff --git a/.nvmrc b/.nvmrc index 9e15be3879..5f09eed8d2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.20.0 +v18.20.3 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..0c69b679f8 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 @@ -337,10 +340,13 @@ export default class Main extends BaseService { const ledgerService = LedgerService.create() + const gridplusService = GridplusService.create() + const signingService = SigningService.create( internalSignerService, ledgerService, chainService, + gridplusService, ) const analyticsService = AnalyticsService.create( @@ -406,6 +412,7 @@ export default class Main extends BaseService { await islandService, await telemetryService, await ledgerService, + await gridplusService, await signingService, await analyticsService, await nftsService, @@ -477,6 +484,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 +627,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 +652,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 +676,7 @@ export default class Main extends BaseService { this.connectIslandService() this.connectTelemetryService() this.connectLedgerService() + this.connectGridplusService() this.connectSigningService() this.connectAnalyticsService() this.connectWalletConnectService() @@ -790,6 +805,67 @@ 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, + startPath, + }: { + n?: number + startPath?: number[] + }) { + return this.gridplusService.fetchAddresses({ n, startPath }) + } + + async importGridPlusAddresses({ + addresses, + }: { + addresses: GridPlusAddress[] + }) { + const trackedNetworks = await this.chainService.getTrackedNetworks() + await Promise.all( + addresses.map(async (address) => { + await this.gridplusService.importAddresses({ address }) + await Promise.all( + trackedNetworks.map(async (network) => { + const addressNetwork = { + address: address.address, + network, + } + await this.chainService.addAccountToTrack(addressNetwork) + this.abilitiesService.getNewAccountAbilities(address.address) + this.store.dispatch(loadAccount(addressNetwork)) + }), + ) + }), + ) + this.store.dispatch( + setNewSelectedAccount({ + address: addresses[0].address, + network: + await this.internalEthereumProviderService.getCurrentOrDefaultNetworkForOrigin( + TAHO_INTERNAL_ORIGIN, + ), + }), + ) + } + + async readActiveGridPlusAddresses(): Promise { + return this.gridplusService.readAddresses() + } + async getAccountEthBalanceUncached( addressNetwork: AddressOnNetwork, ): Promise { @@ -1165,6 +1241,10 @@ export default class Main extends BaseService { this.ledgerService.emitter.on("address", ({ address }) => this.signingService.addTrackedAddress(address, "ledger"), ) + + this.gridplusService.emitter.on("address", ({ address }) => + this.signingService.addTrackedAddress(address, "gridplus"), + ) } async connectLedgerService(): Promise { @@ -1197,6 +1277,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 +1909,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..f7b6c7f70e 100644 --- a/background/package.json +++ b/background/package.json @@ -24,8 +24,8 @@ "watch": "webpack --mode=development --watch" }, "dependencies": { - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", + "@ethereumjs/common": "^4.1.0", + "@ethereumjs/tx": "^5.0.0", "@ethersproject/abstract-provider": "5.7.0", "@ethersproject/abstract-signer": "5.7.0", "@ethersproject/networks": "5.7.1", @@ -58,9 +58,11 @@ "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", + "rlp": "^3.0.0", "sinon": "^14.0.0", "siwe": "^1.1.0", "util": "^0.12.4", diff --git a/background/redux-slices/accounts.ts b/background/redux-slices/accounts.ts index 0dffc8789d..d0723d4b42 100644 --- a/background/redux-slices/accounts.ts +++ b/background/redux-slices/accounts.ts @@ -33,6 +33,7 @@ export const enum AccountType { PrivateKey = "private-key", Imported = "imported", Ledger = "ledger", + GridPlus = "gridplus", Internal = "internal", } @@ -41,6 +42,7 @@ export const accountTypes = [ AccountType.Imported, AccountType.PrivateKey, AccountType.Ledger, + AccountType.GridPlus, AccountType.ReadOnly, ] diff --git a/background/redux-slices/gridplus.ts b/background/redux-slices/gridplus.ts new file mode 100644 index 0000000000..1785bba89d --- /dev/null +++ b/background/redux-slices/gridplus.ts @@ -0,0 +1,98 @@ +import { createSlice } from "@reduxjs/toolkit" +import { createBackgroundAsyncThunk } from "./utils" +import { type GridPlusAddress } from "../services/gridplus" + +const MOCKED_ONBOARDING = process.env.MOCKED_GRIDPLUS_ONBOARDING === "true" + +export type GridPlusState = { + importableAddresses: string[] + activeAddresses: GridPlusAddress[] +} + +export const initialState: GridPlusState = { + importableAddresses: [], + activeAddresses: [], +} + +const gridplusSlice = createSlice({ + name: "gridplus", + initialState, + reducers: { + resetGridPlusState: (immerState) => { + immerState.importableAddresses = [] + immerState.activeAddresses = [] + }, + setImportableAddresses: ( + immerState, + { payload: importableAddresses }: { payload: string[] }, + ) => { + immerState.importableAddresses = importableAddresses + }, + setActiveAddresses: ( + immerState, + { payload: activeAddresses }: { payload: GridPlusAddress[] }, + ) => { + immerState.activeAddresses = activeAddresses + }, + }, +}) + +export const { + resetGridPlusState, + setImportableAddresses, + setActiveAddresses, +} = gridplusSlice.actions + +export default gridplusSlice.reducer + +export const connectGridplus = createBackgroundAsyncThunk( + "gridplus/connect", + async ( + { deviceId, password }: { deviceId?: string; password?: string }, + { extra: { main } }, + ) => main.connectGridplus({ deviceId, password }), +) + +export const pairGridplusDevice = createBackgroundAsyncThunk( + "gridplus/pairDevice", + async ({ pairingCode }: { pairingCode: string }, { extra: { main } }) => + 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 } }, + ) => { + if (MOCKED_ONBOARDING) { + return dispatch( + setImportableAddresses(["0xdfb2682febe6ea96682b1018702958980449b7db"]), + ) + } + return dispatch( + setImportableAddresses( + await main.fetchGridPlusAddresses({ n, startPath }), + ), + ) + }, +) + +export const importGridPlusAddresses = createBackgroundAsyncThunk( + "gridplus/importAddresses", + async ( + { addresses }: { addresses: GridPlusAddress[] }, + { extra: { main } }, + ) => main.importGridPlusAddresses({ addresses }), +) + +export const initializeActiveAddresses = createBackgroundAsyncThunk( + "gridplus/initializeActiveAddresses", + async (_, { extra: { main }, dispatch }) => { + const activeAddresses = await main.readActiveGridPlusAddresses() + return dispatch(setActiveAddresses(activeAddresses)) + }, +) 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/redux-slices/selectors/accountsSelectors.ts b/background/redux-slices/selectors/accountsSelectors.ts index 95bf3b1ef6..463cc194e4 100644 --- a/background/redux-slices/selectors/accountsSelectors.ts +++ b/background/redux-slices/selectors/accountsSelectors.ts @@ -352,6 +352,8 @@ function signerIdFor(accountSigner: AccountSigner): string | null { return accountSigner.keyringID case "ledger": return accountSigner.deviceID + case "gridplus": + return accountSigner.path.join("/") case "read-only": return null default: @@ -365,6 +367,7 @@ const signerTypeToAccountType: Record = { keyring: AccountType.Imported, "private-key": AccountType.PrivateKey, ledger: AccountType.Ledger, + gridplus: AccountType.GridPlus, "read-only": AccountType.ReadOnly, } diff --git a/background/redux-slices/selectors/signingSelectors.ts b/background/redux-slices/selectors/signingSelectors.ts index 6461d5feab..f925bff4fd 100644 --- a/background/redux-slices/selectors/signingSelectors.ts +++ b/background/redux-slices/selectors/signingSelectors.ts @@ -13,6 +13,7 @@ import { selectPrivateKeyWalletsByAddress, } from "./internalSignerSelectors" import { selectCurrentAccount } from "./uiSelectors" +import { GridPlusAccountSigner } from "../../services/gridplus" // FIXME: This has a duplicate in `accountSelectors.ts`, but importing causes a dependency cycle const getAllAddresses = createSelector( @@ -31,11 +32,13 @@ export const selectAccountSignersByAddress = createSelector( (state: RootState) => state.ledger.devices, selectKeyringsByAddresses, selectPrivateKeyWalletsByAddress, + (state: RootState) => state.gridplus.activeAddresses, ( allAddresses, ledgerDevices, keyringsByAddress, privateKeyWalletsByAddress, + gridPlusAddresses, ) => { const allAccountsSeen = new Set() const ledgerEntries = Object.values(ledgerDevices).flatMap((device) => @@ -101,10 +104,18 @@ export const selectAccountSignersByAddress = createSelector( .filter((address) => !allAccountsSeen.has(address)) .map((address) => [address, ReadOnlyAccountSigner]) + const gridPlusEntries = gridPlusAddresses.map( + (address): [HexString, GridPlusAccountSigner] => [ + address.address, + { type: "gridplus", path: address.path }, + ], + ) + const entriesByPriority: [string, AccountSigner][] = [ ...readOnlyEntries, ...privateKeyEntries, ...ledgerEntries, + ...gridPlusEntries, // Give priority to keyring over Ledger and private key, if an address is signable by // both. ...keyringEntries, diff --git a/background/services/gridplus/index.ts b/background/services/gridplus/index.ts new file mode 100644 index 0000000000..ab0f78796f --- /dev/null +++ b/background/services/gridplus/index.ts @@ -0,0 +1,267 @@ +import { storage } from "webextension-polyfill" +import * as GridPlusSdk from "gridplus-sdk" +import { TransactionFactory, type FeeMarketEIP1559TxData } from "@ethereumjs/tx" +import { hexlify, joinSignature } from "ethers/lib/utils" +import { BigNumber } from "ethers" +import { + UnsignedTransaction, + parse, + serialize, +} from "@ethersproject/transactions" +import { encode } from "rlp" +import { SignedTransaction, TransactionRequestWithNonce } from "../../networks" +import { ethersTransactionFromTransactionRequest } from "../chain/utils" +import { EIP712TypedData, HexString } from "../../types" +import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" +import BaseService from "../base" + +const APP_NAME = "Taho Wallet" +const CLIENT_STORAGE_KEY = "GRIDPLUS_CLIENT" +const ADDRESSES_STORAGE_KEY = "GRIDPLUS_ADDRESSES" + +const writeClient = (client: GridplusClient) => + storage.local.set({ + [CLIENT_STORAGE_KEY]: client, + }) + +const writeAddresses = (addresses: GridPlusAddress[]) => + storage.local.set({ + [ADDRESSES_STORAGE_KEY]: JSON.stringify(addresses), + }) + +export type GridPlusAccountSigner = { + type: "gridplus" + path: number[] +} + +export type GridPlusAddress = { + address: string + addressIndex: number + path: number[] +} + +type GridplusClient = string | null + +interface Events extends ServiceLifecycleEvents { + address: { gridplusIndex: number; derivationPath: number[]; address: string } +} + +const addHexPrefix = (data: string) => `0x${data}` + +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 readAddresses() { + const persistedAddresses = + (await storage.local.get(ADDRESSES_STORAGE_KEY))?.[ + ADDRESSES_STORAGE_KEY + ] ?? "[]" + const activeAddresses = JSON.parse(persistedAddresses) + this.activeAddresses = activeAddresses + return this.activeAddresses + } + + async setupClient({ + deviceId, + password, + }: { + deviceId?: string + password?: string + }) { + return GridPlusSdk.setup({ + deviceId, + password, + name: APP_NAME, + getStoredClient: () => this.client ?? "", + setStoredClient: writeClient, + }) + } + + async pairDevice({ pairingCode }: { pairingCode: string }) { + await this.readClient() + return GridPlusSdk.pair(pairingCode) + } + + async fetchAddresses({ + n = 10, + startPath = [0x80000000 + 44, 0x80000000 + 60, 0x80000000, 0, 0], + }: { + n?: number + startPath?: number[] + }) { + await this.readClient() + return GridPlusSdk.fetchAddresses({ n, startPath }) + } + + async importAddresses({ address }: { address: GridPlusAddress }) { + this.activeAddresses.push(address) + await writeAddresses(this.activeAddresses) + this.emitter.emit("address", { + gridplusIndex: address.addressIndex, + derivationPath: address.path, + address: address.address, + }) + } + + async signMessage( + { address }: { address: HexString }, + hexDataToSign: HexString, + ) { + const accounts = await this.readAddresses() + const accountData = accounts.find((account) => account.address === address) + const response = await GridPlusSdk.signMessage(hexDataToSign, { + signerPath: accountData?.path, + payload: hexDataToSign, + protocol: "signPersonal", + }) + const responseAddress = hexlify(response.signer) + if (responseAddress.toLowerCase() !== address.toLowerCase()) { + throw new Error( + "GridPlus returned a different address than the one requested", + ) + } + if (!response.sig) { + throw new Error("GridPlus returned an error") + } + const signature = joinSignature({ + r: addHexPrefix(response.sig.r.toString("hex")), + s: addHexPrefix(response.sig.s.toString("hex")), + v: BigNumber.from( + addHexPrefix(response.sig.v.toString("hex")), + ).toNumber(), + }) + return signature + } + + async signTypedData( + { address }: { address: HexString }, + typedData: EIP712TypedData, + ) { + if ( + typeof typedData !== "object" || + !(typedData.types || typedData.primaryType || typedData.domain) + ) { + throw new Error("unsupported typed data version") + } + const accounts = await this.readAddresses() + const accountData = accounts.find((account) => account.address === address) + const eip712Data = { + types: typedData.types, + primaryType: typedData.primaryType, + domain: typedData.domain, + message: typedData.message, + } + const response = await GridPlusSdk.signMessage(eip712Data, { + signerPath: accountData?.path, + protocol: "eip712", + payload: eip712Data, + }) + const responseAddress = hexlify(response.signer) + if (responseAddress.toLowerCase() !== address.toLowerCase()) { + throw new Error( + "Address not found on this wallet. Try another SafeCard or remove the SafeCard to use the wallet on your device.", + ) + } + if (!response.sig) { + throw new Error("GridPlus returned an error") + } + const signature = joinSignature({ + r: addHexPrefix(response.sig.r.toString("hex")), + s: addHexPrefix(response.sig.s.toString("hex")), + v: BigNumber.from( + addHexPrefix(response.sig.v.toString("hex")), + ).toNumber(), + }) + return signature + } + + async signTransaction( + { address }: { address: HexString }, + transactionRequest: TransactionRequestWithNonce, + ): Promise { + const ethersTx = ethersTransactionFromTransactionRequest({ + ...transactionRequest, + from: address, + }) + const accounts = await this.readAddresses() + const accountData = accounts.find((account) => account.address === address) + const txPayload = TransactionFactory.fromTxData( + ethersTx as FeeMarketEIP1559TxData, + ) + const signPayload = { + data: { + signerPath: accountData?.path, + chain: ethersTx.chainId, + curveType: GridPlusSdk.Constants.SIGNING.CURVES.SECP256K1, + hashType: GridPlusSdk.Constants.SIGNING.HASHES.KECCAK256, + encodingType: GridPlusSdk.Constants.SIGNING.ENCODINGS.EVM, + payload: + ethersTx.type === 2 + ? txPayload.getMessageToSign() + : encode(txPayload.getMessageToSign()), + }, + } + const response = await GridPlusSdk.sign([], signPayload) + const r = addHexPrefix(response.sig.r.toString("hex")) + const s = addHexPrefix(response.sig.s.toString("hex")) + const v = BigNumber.from( + addHexPrefix(response.sig.v.toString("hex")), + ).toNumber() + if (response.pubkey) { + const serializedTransaction = serialize(ethersTx as UnsignedTransaction, { + r, + s, + v, + }) + const parsedTx = parse(serializedTransaction) + if (parsedTx.from?.toLowerCase() !== address?.toLowerCase()) { + throw new Error( + "Address not found on this wallet. Try another SafeCard or remove the SafeCard to use the wallet on your device.", + ) + } + return { + hash: parsedTx.hash ?? "", + from: parsedTx.from, + to: parsedTx.to, + nonce: parsedTx.nonce, + input: parsedTx.data, + value: parsedTx.value.toBigInt(), + type: (parsedTx.type ?? null) as never, + gasPrice: parsedTx.gasPrice ? parsedTx.gasPrice.toBigInt() : null, + maxFeePerGas: parsedTx.maxFeePerGas + ? parsedTx.maxFeePerGas.toBigInt() + : null, + maxPriorityFeePerGas: parsedTx.maxPriorityFeePerGas + ? parsedTx.maxPriorityFeePerGas.toBigInt() + : null, + gasLimit: parsedTx.gasLimit.toBigInt(), + r, + s, + v, + blockHash: null, + blockHeight: null, + asset: transactionRequest.network.baseAsset, + network: transactionRequest.network, + } + } + throw new Error("error signing transaction with gridplus") + } +} 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/background/services/internal-ethereum-provider/index.ts b/background/services/internal-ethereum-provider/index.ts index 495a528ef1..4c05215297 100644 --- a/background/services/internal-ethereum-provider/index.ts +++ b/background/services/internal-ethereum-provider/index.ts @@ -503,10 +503,13 @@ export default class InternalEthereumProviderService extends BaseService // Ethers does not want to see the EIP712Domain field, extract it. const { EIP712Domain, ...typesForSigning } = params.typedData.types + const { domain } = params.typedData + domain.chainId = params.account.network.chainID + // Ask Ethers to give us a filtered payload that only includes types // specified in the `types` object. const filteredTypedDataPayload = _TypedDataEncoder.getPayload( - params.typedData.domain, + domain, typesForSigning, params.typedData.message, ) diff --git a/background/services/preferences/db.ts b/background/services/preferences/db.ts index 831b1d8a74..9663f07180 100644 --- a/background/services/preferences/db.ts +++ b/background/services/preferences/db.ts @@ -21,6 +21,8 @@ const getSignerRecordId = (signer: AccountSignerWithId): SignerRecordId => { return `${signer.type}/${signer.keyringID}` case "private-key": return `${signer.type}/${signer.walletID}` + case "gridplus": + return `${signer.type}/${signer.path}` default: return `${signer.type}/${signer.deviceID}` } diff --git a/background/services/signing/index.ts b/background/services/signing/index.ts index 1fb808626c..5088c2e94f 100644 --- a/background/services/signing/index.ts +++ b/background/services/signing/index.ts @@ -15,6 +15,7 @@ import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" import ChainService from "../chain" import { AddressOnNetwork } from "../../accounts" import { assertUnreachable } from "../../lib/utils/type-guards" +import GridplusService, { GridPlusAccountSigner } from "../gridplus" type SigningErrorReason = "userRejected" | "genericError" type ErrorResponse = { @@ -59,7 +60,7 @@ export type AccountSigner = | PrivateKeyAccountSigner | KeyringAccountSigner | HardwareAccountSigner -export type HardwareAccountSigner = LedgerAccountSigner +export type HardwareAccountSigner = LedgerAccountSigner | GridPlusAccountSigner export type SignerType = AccountSigner["type"] @@ -99,18 +100,26 @@ export default class SigningService extends BaseService { Promise, Promise, Promise, + Promise, ] - > = async (internalSignerService, ledgerService, chainService) => + > = async ( + internalSignerService, + ledgerService, + chainService, + gridplusService, + ) => new this( await internalSignerService, await ledgerService, await chainService, + await gridplusService, ) private constructor( private internalSignerService: InternalSignerService, private ledgerService: LedgerService, private chainService: ChainService, + private gridplusService: GridplusService, ) { super() } @@ -141,6 +150,11 @@ export default class SigningService extends BaseService { transactionWithNonce, accountSigner, ) + case "gridplus": + return this.gridplusService.signTransaction( + { address: transactionWithNonce.from }, + transactionWithNonce, + ) case "private-key": case "keyring": return this.internalSignerService.signTransaction( @@ -170,6 +184,9 @@ export default class SigningService extends BaseService { case "ledger": await this.ledgerService.removeAddress(address) break + case "gridplus": + // FIXME: implement + break case "read-only": break // no additional work here, just account removal below default: @@ -267,6 +284,12 @@ export default class SigningService extends BaseService { accountSigner, ) break + case "gridplus": + signedData = await this.gridplusService.signTypedData( + { address: account.address }, + typedData, + ) + break case "private-key": case "keyring": signedData = await this.internalSignerService.signTypedData({ @@ -313,6 +336,12 @@ export default class SigningService extends BaseService { hexDataToSign, ) break + case "gridplus": + signedData = await this.gridplusService.signMessage( + addressOnNetwork, + hexDataToSign, + ) + break case "private-key": case "keyring": signedData = await this.internalSignerService.personalSign({ diff --git a/background/tests/factories.ts b/background/tests/factories.ts index e9736f7f70..372478ae79 100644 --- a/background/tests/factories.ts +++ b/background/tests/factories.ts @@ -41,6 +41,7 @@ import { AccountData, CompleteAssetAmount } from "../redux-slices/accounts" import { AnalyticsService, ChainService, + GridplusService, IndexingService, InternalEthereumProviderService, InternalSignerService, @@ -120,10 +121,14 @@ export async function createIndexingService(overrides?: { export const createLedgerService = async (): Promise => LedgerService.create() +export const createGridPlusService = async (): Promise => + GridplusService.create() + type CreateSigningServiceOverrides = { internalSignerService?: Promise ledgerService?: Promise chainService?: Promise + gridPlusService?: Promise } type CreateAbilitiesServiceOverrides = { @@ -148,6 +153,7 @@ export const createSigningService = async ( overrides.internalSignerService ?? createInternalSignerService(), overrides.ledgerService ?? createLedgerService(), overrides.chainService ?? createChainService(), + overrides.gridPlusService ?? createGridPlusService(), ) export async function createAnalyticsService(overrides?: { diff --git a/ci/hardhat.config.ts b/ci/hardhat.config.ts index 3b05e927bf..0795466d93 100644 --- a/ci/hardhat.config.ts +++ b/ci/hardhat.config.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-unresolved */ import { HardhatUserConfig } from "hardhat/config" import "dotenv-defaults/config" 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