diff --git a/CHANGELOG.md b/CHANGELOG.md index db6d61a1..82b40625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.1.0] +### Added +- Add warning for callers of `wallet_watchAsset` with ERC721 and ERC1155 token types, that support is currently considered experimental ([#264](https://github.com/MetaMask/providers/pull/264)) + ## [11.0.0] ### Changed - **BREAKING**: Minimum Node.js version 16 ([#254](https://github.com/MetaMask/providers/pull/254)) @@ -220,7 +224,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 added deprecation warnings for them ([#30](https://github.com/MetaMask/providers/pull/30)) - Un-deprecated `sendAsync` ([#29](https://github.com/MetaMask/providers/pull/29)) -[Unreleased]: https://github.com/MetaMask/providers/compare/v11.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/providers/compare/v11.1.0...HEAD +[11.1.0]: https://github.com/MetaMask/providers/compare/v11.0.0...v11.1.0 [11.0.0]: https://github.com/MetaMask/providers/compare/v10.2.1...v11.0.0 [10.2.1]: https://github.com/MetaMask/providers/compare/v10.2.0...v10.2.1 [10.2.0]: https://github.com/MetaMask/providers/compare/v10.1.0...v10.2.0 diff --git a/jest.config.js b/jest.config.js index aed0590e..586a33ef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -45,10 +45,10 @@ const baseConfig = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 63.07, - functions: 64.13, - lines: 65.77, - statements: 65.87, + branches: 64.25, + functions: 64.89, + lines: 66.35, + statements: 66.43, }, }, diff --git a/package.json b/package.json index 5eaa6f25..516a976d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/providers", - "version": "11.0.0", + "version": "11.1.0", "description": "A JavaScript Ethereum provider that connects to the wallet over a stream.", "keywords": [ "MetaMask", diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..b80c0dcd --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +// TOKEN STANDARDS +export const ERC721 = 'ERC721'; +export const ERC1155 = 'ERC1155'; +export const ERC20 = 'ERC20'; diff --git a/src/messages.ts b/src/messages.ts index 5eebff76..ca5c4bb0 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -37,6 +37,7 @@ const messages = { rpc: { ethDecryptDeprecation: `MetaMask: The RPC method 'eth_decrypt' is deprecated and may be removed in the future.\nFor more information, see: https://medium.com/metamask/metamask-api-method-deprecation-2b0564a84686`, ethGetEncryptionPublicKeyDeprecation: `MetaMask: The RPC method 'eth_getEncryptionPublicKey' is deprecated and may be removed in the future.\nFor more information, see: https://medium.com/metamask/metamask-api-method-deprecation-2b0564a84686`, + walletWatchAssetNFTExperimental: `MetaMask: The RPC method 'wallet_watchAsset' is experimental for ERC721/ERC1155 assets and may change in the future.\nFor more information, see: https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-1.md and https://github.com/MetaMask/metamask-improvement-proposals/blob/main/PROCESS-GUIDE.md#proposal-lifecycle`, }, // misc experimentalMethods: `MetaMask: 'ethereum._metamask' exposes non-standard, experimental methods. They may be removed or changed without warning.`, diff --git a/src/middleware/createRpcWarningMiddleware.test.ts b/src/middleware/createRpcWarningMiddleware.test.ts index ea7e96fa..4e638515 100644 --- a/src/middleware/createRpcWarningMiddleware.test.ts +++ b/src/middleware/createRpcWarningMiddleware.test.ts @@ -3,90 +3,123 @@ import { JsonRpcEngine, JsonRpcFailure, JsonRpcSuccess } from 'json-rpc-engine'; import { createRpcWarningMiddleware } from './createRpcWarningMiddleware'; import messages from '../messages'; -const warnings = [ +const affected = [ { + scenario: 'eth_decrypt', method: 'eth_decrypt', warning: messages.warnings.rpc.ethDecryptDeprecation, }, { + scenario: 'eth_getEncryptionPublicKey', method: 'eth_getEncryptionPublicKey', warning: messages.warnings.rpc.ethGetEncryptionPublicKeyDeprecation, }, + { + scenario: 'wallet_watchAsset with `type: "ERC721"`', + method: 'wallet_watchAsset', + params: { + type: 'ERC721', + options: {}, + }, + warning: messages.warnings.rpc.walletWatchAssetNFTExperimental, + }, + { + scenario: 'wallet_watchAsset with `type: "ERC1155"`', + method: 'wallet_watchAsset', + params: { + type: 'ERC1155', + options: {}, + }, + warning: messages.warnings.rpc.walletWatchAssetNFTExperimental, + }, +]; + +const unaffected = [ + { + scenario: 'eth_chainId', + method: 'eth_chainId', + }, + { + scenario: 'wallet_watchAsset with `type: "ERC20"`', + method: 'wallet_watchAsset', + params: { + type: 'ERC20', + options: {}, + }, + }, ]; describe('createRpcWarningMiddleware', () => { - for (const { method, warning } of warnings) { - describe(`${method}`, () => { - it('should warn the first time the method is called', async () => { - const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn'); - const middleware = createRpcWarningMiddleware(globalThis.console); - const engine = new JsonRpcEngine(); - engine.push(middleware); - - await engine.handle({ jsonrpc: '2.0', id: 1, method }); - - expect(consoleWarnSpy).toHaveBeenCalledWith(warning); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - }); + describe.each(affected)('$scenario', ({ method, params = {}, warning }) => { + it('should warn the first time the method is called', async () => { + const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn'); + const middleware = createRpcWarningMiddleware(globalThis.console); + const engine = new JsonRpcEngine(); + engine.push(middleware); - it('should not warn the second time the method is called', async () => { - const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn'); - const middleware = createRpcWarningMiddleware(globalThis.console); - const engine = new JsonRpcEngine(); - engine.push(middleware); + await engine.handle({ jsonrpc: '2.0', id: 1, method, params }); - await engine.handle({ jsonrpc: '2.0', id: 1, method }); - await engine.handle({ jsonrpc: '2.0', id: 1, method }); + expect(consoleWarnSpy).toHaveBeenCalledWith(warning); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + }); - expect(consoleWarnSpy).toHaveBeenCalledWith(warning); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - }); + it('should not warn the second time the method is called', async () => { + const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn'); + const middleware = createRpcWarningMiddleware(globalThis.console); + const engine = new JsonRpcEngine(); + engine.push(middleware); - it('should allow the method to succeed', async () => { - const middleware = createRpcWarningMiddleware(globalThis.console); - const engine = new JsonRpcEngine(); - engine.push(middleware); - engine.push((_req, res, _next, end) => { - res.result = 'success!'; - end(); - }); - - const response = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method, - })) as JsonRpcSuccess; - - expect(response.result).toBe('success!'); + await engine.handle({ jsonrpc: '2.0', id: 1, method, params }); + await engine.handle({ jsonrpc: '2.0', id: 1, method, params }); + + expect(consoleWarnSpy).toHaveBeenCalledWith(warning); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + }); + + it('should allow the method to succeed', async () => { + const middleware = createRpcWarningMiddleware(globalThis.console); + const engine = new JsonRpcEngine(); + engine.push(middleware); + engine.push((_req, res, _next, end) => { + res.result = 'success!'; + end(); }); - it('should allow the method to fail', async () => { - const middleware = createRpcWarningMiddleware(globalThis.console); - const engine = new JsonRpcEngine(); - engine.push(middleware); - engine.push(() => { - throw new Error('Failure!'); - }); - - const result = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method, - })) as JsonRpcFailure; - - expect(result.error.message).toBe('Failure!'); + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method, + })) as JsonRpcSuccess; + + expect(response.result).toBe('success!'); + }); + + it('should allow the method to fail', async () => { + const middleware = createRpcWarningMiddleware(globalThis.console); + const engine = new JsonRpcEngine(); + engine.push(middleware); + engine.push(() => { + throw new Error('Failure!'); }); + + const result = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method, + })) as JsonRpcFailure; + + expect(result.error.message).toBe('Failure!'); }); - } + }); - describe('unaffected method', () => { + describe.each(unaffected)('$scenario', ({ method, params = {} }) => { it('should not issue a warning', async () => { const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn'); const middleware = createRpcWarningMiddleware(globalThis.console); const engine = new JsonRpcEngine(); engine.push(middleware); - await engine.handle({ jsonrpc: '2.0', id: 1, method: 'eth_chainId' }); + await engine.handle({ jsonrpc: '2.0', id: 1, method, params }); expect(consoleWarnSpy).not.toHaveBeenCalled(); }); @@ -103,7 +136,8 @@ describe('createRpcWarningMiddleware', () => { const response = (await engine.handle({ jsonrpc: '2.0', id: 1, - method: 'eth_chainId', + method, + params, })) as JsonRpcSuccess; expect(response.result).toBe('success!'); @@ -120,7 +154,8 @@ describe('createRpcWarningMiddleware', () => { const result = (await engine.handle({ jsonrpc: '2.0', id: 1, - method: 'eth_chainId', + method, + params, })) as JsonRpcFailure; expect(result.error.message).toBe('Failure!'); diff --git a/src/middleware/createRpcWarningMiddleware.ts b/src/middleware/createRpcWarningMiddleware.ts index c3f3071f..be57d293 100644 --- a/src/middleware/createRpcWarningMiddleware.ts +++ b/src/middleware/createRpcWarningMiddleware.ts @@ -1,5 +1,6 @@ -import type { JsonRpcMiddleware } from 'json-rpc-engine'; +import type { JsonRpcMiddleware, JsonRpcRequest } from 'json-rpc-engine'; +import { ERC1155, ERC721 } from '../constants'; import messages from '../messages'; import type { ConsoleLike } from '../utils'; @@ -15,6 +16,7 @@ export function createRpcWarningMiddleware( const sentWarnings = { ethDecryptDeprecation: false, ethGetEncryptionPublicKeyDeprecation: false, + walletWatchAssetNFTExperimental: false, }; return (req, _res, next) => { @@ -27,6 +29,15 @@ export function createRpcWarningMiddleware( ) { log.warn(messages.warnings.rpc.ethGetEncryptionPublicKeyDeprecation); sentWarnings.ethGetEncryptionPublicKeyDeprecation = true; + } else if ( + !sentWarnings.walletWatchAssetNFTExperimental && + req.method === 'wallet_watchAsset' && + [ERC721, ERC1155].includes( + (req as JsonRpcRequest<{ type: string }>).params?.type || '', + ) + ) { + log.warn(messages.warnings.rpc.walletWatchAssetNFTExperimental); + sentWarnings.walletWatchAssetNFTExperimental = true; } next(); };