diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 0389b84ecb8..5b2329c0436 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -324,13 +324,25 @@ export const getRpcMethodMiddleware = ({ eth_coinbase: getEthAccounts, eth_sendTransaction: async () => { checkTabActive(); - await checkActiveAccountAndChainId({ - hostname, - address: req.params[0].from, - chainId: req.params[0].chainId, - checkSelectedAddress: isMMSDK || isWalletConnect, + return RPCMethods.eth_sendTransaction({ + next, + req, + res, + validateAccountAndChainId: async ({ + from, + chainId, + }: { + from?: string; + chainId?: number; + }) => { + await checkActiveAccountAndChainId({ + hostname, + address: from, + chainId, + checkSelectedAddress: isMMSDK || isWalletConnect, + }); + }, }); - next(); }, eth_signTransaction: async () => { // This is implemented later in our middleware stack – specifically, in diff --git a/app/core/RPCMethods/eth_sendTransaction.test.ts b/app/core/RPCMethods/eth_sendTransaction.test.ts new file mode 100644 index 00000000000..01667ca4e73 --- /dev/null +++ b/app/core/RPCMethods/eth_sendTransaction.test.ts @@ -0,0 +1,110 @@ +// eslint-disable-next-line import/no-nodejs-modules +import { inspect } from 'util'; +import type { JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; +import eth_sendTransaction from './eth_sendTransaction'; + +/** + * Construct a `eth_sendTransaction` JSON-RPC request. + * + * @param params - The request parameters. + * @returns The JSON-RPC request. + */ +function constructSendTransactionRequest( + params: unknown, +): JsonRpcRequest & { method: 'eth_sendTransaction' } { + return { + jsonrpc: '2.0', + id: 1, + method: 'eth_sendTransaction', + params, + }; +} + +/** + * Construct a pending JSON-RPC response. + * + * @returns A pending JSON-RPC response. + */ +function constructPendingJsonRpcResponse(): PendingJsonRpcResponse { + return { + jsonrpc: '2.0', + id: 1, + }; +} + +describe('eth_sendTransaction', () => { + it('invokes next middleware for a valid request', async () => { + const nextMock = jest.fn(); + const minimalValidParams = [{}]; + + await eth_sendTransaction({ + next: nextMock, + req: constructSendTransactionRequest(minimalValidParams), + res: constructPendingJsonRpcResponse(), + validateAccountAndChainId: jest.fn(), + }); + + expect(nextMock).toHaveBeenCalledTimes(1); + }); + + const invalidParameters = [null, undefined, '', {}]; + for (const invalidParameter of invalidParameters) { + it(`throws a JSON-RPC invalid parameters error if given "${inspect( + invalidParameter, + )}"`, async () => { + const nextMock = jest.fn(); + + await expect( + async () => + await eth_sendTransaction({ + next: nextMock, + req: constructSendTransactionRequest(invalidParameter), + res: constructPendingJsonRpcResponse(), + validateAccountAndChainId: jest.fn(), + }), + ).rejects.toThrow('Invalid parameters: expected an array'); + expect(nextMock).not.toHaveBeenCalled(); + }); + } + + const invalidTransactionParameters = [null, undefined, '', []]; + for (const invalidTransactionParameter of invalidTransactionParameters) { + it(`throws a JSON-RPC invalid parameters error if given "${inspect( + invalidTransactionParameter, + )}" transaction parameters`, async () => { + const nextMock = jest.fn(); + const invalidParameter = [invalidTransactionParameter]; + + await expect( + async () => + await eth_sendTransaction({ + next: nextMock, + req: constructSendTransactionRequest(invalidParameter), + res: constructPendingJsonRpcResponse(), + validateAccountAndChainId: jest.fn(), + }), + ).rejects.toThrow( + 'Invalid parameters: expected the first parameter to be an object', + ); + expect(nextMock).not.toHaveBeenCalled(); + }); + } + + it('throws any validation errors', async () => { + const nextMock = jest.fn(); + const minimalValidParams = [{}]; + + await expect( + async () => + await eth_sendTransaction({ + next: nextMock, + req: constructSendTransactionRequest(minimalValidParams), + res: constructPendingJsonRpcResponse(), + validateAccountAndChainId: jest.fn().mockImplementation(async () => { + throw new Error('test validation error'); + }), + }), + ).rejects.toThrow('test validation error'); + expect(nextMock).not.toHaveBeenCalled(); + }); +}); diff --git a/app/core/RPCMethods/eth_sendTransaction.ts b/app/core/RPCMethods/eth_sendTransaction.ts new file mode 100644 index 00000000000..0a4b686e78d --- /dev/null +++ b/app/core/RPCMethods/eth_sendTransaction.ts @@ -0,0 +1,55 @@ +import type { + AsyncJsonRpcEngineNextCallback, + JsonRpcRequest, + PendingJsonRpcResponse, +} from 'json-rpc-engine'; +import { isObject, hasProperty } from '@metamask/utils'; +import { ethErrors } from 'eth-json-rpc-errors'; + +/** + * Handle a `eth_sendTransaction` request. + * + * @param args - Named arguments. + * @param args.checkActiveAccountAndChainId - A function that validates the account and chain ID + * used in the transaction. + * @param args.req - The JSON-RPC request. + * @param args.res - The JSON-RPC response. + */ +async function eth_sendTransaction({ + next, + req, + res: _res, + validateAccountAndChainId, +}: { + validateAccountAndChainId: (args: { + from: string; + chainId?: number; + }) => Promise; + next: AsyncJsonRpcEngineNextCallback; + req: JsonRpcRequest & { method: 'eth_sendTransaction' }; + res: PendingJsonRpcResponse; +}) { + if ( + !Array.isArray(req.params) && + !(isObject(req.params) && hasProperty(req.params, 0)) + ) { + throw ethErrors.rpc.invalidParams({ + message: `Invalid parameters: expected an array`, + }); + } + const transactionParameters = req.params[0]; + if (!isObject(transactionParameters)) { + throw ethErrors.rpc.invalidParams({ + message: `Invalid parameters: expected the first parameter to be an object`, + }); + } + await validateAccountAndChainId({ + from: req.params[0].from, + chainId: req.params[0].chainId, + }); + + // This is handled later in the network middleware + next(); +} + +export default eth_sendTransaction; diff --git a/app/core/RPCMethods/index.js b/app/core/RPCMethods/index.js index 2335997a8b5..3c074804744 100644 --- a/app/core/RPCMethods/index.js +++ b/app/core/RPCMethods/index.js @@ -1,7 +1,9 @@ +import eth_sendTransaction from './eth_sendTransaction'; import wallet_addEthereumChain from './wallet_addEthereumChain.js'; import wallet_switchEthereumChain from './wallet_switchEthereumChain.js'; const RPCMethods = { + eth_sendTransaction, wallet_addEthereumChain, wallet_switchEthereumChain, }; diff --git a/package.json b/package.json index 9a75e41c347..4a980a87396 100644 --- a/package.json +++ b/package.json @@ -152,8 +152,8 @@ "@metamask/message-manager": "^1.0.1", "@metamask/network-controller": "^2.0.0", "@metamask/permission-controller": "^1.0.2", - "@metamask/preferences-controller": "^2.1.0", "@metamask/phishing-controller": "^2.0.0", + "@metamask/preferences-controller": "^2.1.0", "@metamask/sdk-communication-layer": "^0.1.0", "@metamask/swaps-controller": "^6.8.0", "@metamask/transaction-controller": "^2.0.0", @@ -324,6 +324,7 @@ "@metamask/eslint-config": "^7.0.0", "@metamask/eslint-config-typescript": "^7.0.0", "@metamask/mobile-provider": "^2.1.0", + "@metamask/utils": "^5.0.0", "@react-native-community/eslint-config": "^2.0.0", "@rpii/wdio-html-reporter": "^7.7.1", "@storybook/addon-actions": "^5.3", diff --git a/yarn.lock b/yarn.lock index 0527edc66bf..0c6dcfc8b6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1524,6 +1524,27 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@chainsafe/as-sha256@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9" + integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg== + +"@chainsafe/persistent-merkle-tree@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz#4c9ee80cc57cd3be7208d98c40014ad38f36f7ff" + integrity sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + +"@chainsafe/ssz@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497" + integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + "@chainsafe/persistent-merkle-tree" "^0.4.2" + case "^1.6.3" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -1913,11 +1934,24 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.5" +"@ethereumjs/common@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.1.1.tgz#6f754c8933727ad781f63ca3929caab542fe184e" + integrity sha512-iEl4gQtcrj2udNhEizs04z7WA15ez1QoXL0XzaCyaNgwRyXezIg1DnfNeZUUpJnkrOF/0rYXyq2UFSLxt1NPQg== + dependencies: + "@ethereumjs/util" "^8.0.5" + crc-32 "^1.2.0" + "@ethereumjs/rlp@^4.0.0-beta.2": version "4.0.0" resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.0.tgz#66719891bd727251a7f233f9ca80212d1994f8c8" integrity sha512-LM4jS5n33bJN60fM5EC8VeyhUgga6/DjCPBV2vWjnfVtobqtOiNC4SQ1MRFqyBSmJGGdB533JZWewyvlcdJtkQ== +"@ethereumjs/rlp@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" + integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== + "@ethereumjs/tx@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.0.0.tgz#8dfd91ed6e91e63996e37b3ddc340821ebd48c81" @@ -1942,6 +1976,18 @@ "@ethereumjs/common" "^2.4.0" ethereumjs-util "^7.1.0" +"@ethereumjs/tx@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.1.1.tgz#d1b5bf2c4fd3618f2f333b66e262848530d4686a" + integrity sha512-QDj7nuROfoeyK83RObMA0XCZ+LUDdneNkSCIekO498uEKTY25FxI4Whduc/6j0wdd4IqpQvkq+/7vxSULjGIBQ== + dependencies: + "@chainsafe/ssz" "0.9.4" + "@ethereumjs/common" "^3.1.1" + "@ethereumjs/rlp" "^4.0.1" + "@ethereumjs/util" "^8.0.5" + "@ethersproject/providers" "^5.7.2" + ethereum-cryptography "^1.1.2" + "@ethereumjs/util@^8.0.0": version "8.0.2" resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.0.2.tgz#b7348fc7253649b0f00685a94546c6eee1fad819" @@ -1951,6 +1997,15 @@ async "^3.2.4" ethereum-cryptography "^1.1.2" +"@ethereumjs/util@^8.0.5": + version "8.0.5" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.0.5.tgz#b9088fc687cc13f0c1243d6133d145dfcf3fe446" + integrity sha512-259rXKK3b3D8HRVdRmlOEi6QFvwxdt304hhrEAmpZhsj7ufXEOTIc9JRZPMnXatKjECokdLNBcDOFBeBSzAIaw== + dependencies: + "@chainsafe/ssz" "0.9.4" + "@ethereumjs/rlp" "^4.0.1" + ethereum-cryptography "^1.1.2" + "@ethersproject/abi@5.4.1": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.4.1.tgz#6ac28fafc9ef6f5a7a37e30356a2eb31fa05d39b" @@ -2469,7 +2524,7 @@ bech32 "1.1.4" ws "7.4.6" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.0": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.0", "@ethersproject/providers@^5.7.2": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -4300,6 +4355,17 @@ resolved "https://registry.yarnpkg.com/@metamask/types/-/types-1.1.0.tgz#9bd14b33427932833c50c9187298804a18c2e025" integrity sha512-EEV/GjlYkOSfSPnYXfOosxa3TqYtIW3fhg6jdw+cok/OhMgNn4wCfbENFqjytrHMU2f7ZKtBAvtiP5V8H44sSw== +"@metamask/utils@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-5.0.0.tgz#592dea36bb83d8cbf8f12ee236dd9fa49827e5e9" + integrity sha512-bsy7YiGxMo3rvyYbZc33yfMIfX4vVENFcxKctvNjLwCMx9d0xbLPT1T8yiqobb4NkZbwlacYEGxnv3GkTePGig== + dependencies: + "@ethereumjs/tx" "^4.1.1" + "@types/debug" "^4.1.7" + debug "^4.3.4" + semver "^7.3.8" + superstruct "^1.0.3" + "@ngraveio/bc-ur@^1.1.5", "@ngraveio/bc-ur@^1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@ngraveio/bc-ur/-/bc-ur-1.1.6.tgz#8f8c75fff22f6a5e4dfbc5a6b540d7fe8f42cd39" @@ -5443,6 +5509,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + "@types/deep-freeze-strict@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.0.tgz#447a6a2576191344aa42310131dd3df5c41492c4" @@ -5622,6 +5695,11 @@ resolved "https://registry.yarnpkg.com/@types/mockery/-/mockery-1.4.30.tgz#25f07fa7340371c7ee0fb9239511a34e0a19d5b7" integrity sha512-uv53RrNdhbkV/3VmVCtfImfYCWC3GTTRn3R11Whni3EJ+gb178tkZBVNj2edLY5CMrB749dQi+SJkg87jsN8UQ== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node@*": version "16.0.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.1.tgz#70cedfda26af7a2ca073fdcc9beb2fff4aa693f8" @@ -9031,6 +9109,11 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" +case@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" + integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== + caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -21996,7 +22079,7 @@ semver@^7.0.0, semver@^7.3.2, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -semver@^7.2.1: +semver@^7.2.1, semver@^7.3.8: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -23067,6 +23150,11 @@ superagent@^3.8.1: qs "^6.5.1" readable-stream "^2.3.5" +superstruct@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.3.tgz#de626a5b49c6641ff4d37da3c7598e7a87697046" + integrity sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg== + supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"