From b0679f6e45b6ecd8c6e567f569d937f9a8d5b80b Mon Sep 17 00:00:00 2001 From: Ilan <36084092+ilanolkies@users.noreply.github.com> Date: Tue, 22 Mar 2022 07:26:10 -0300 Subject: [PATCH] Allow the developer to manually accept/reject an eth_requestAccounts request (#15) * Refactor: make mock provider a class * Allow to set flag to request enable acceptance * Bump to v1 * Lint --- .eslintrc.json | 6 +- package.json | 3 +- src/index.test.ts | 49 +++++++++++- src/index.ts | 184 +++++++++++++++++++++++++++++----------------- 4 files changed, 167 insertions(+), 75 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index ef2c6d7..8e0b1ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,7 +22,9 @@ "radix": "off", "import/extensions": ["error", "always", { "ts": "never" - }] + }], + "no-unused-vars": "off", // ref: https://github.com/typescript-eslint/typescript-eslint/blob/b5b5f415c234f3456575a69da31ac9f6d2f8b146/packages/eslint-plugin/docs/rules/no-unused-vars.md + "@typescript-eslint/no-unused-vars": ["error"] }, "settings": { "import/resolver": { @@ -30,5 +32,5 @@ "extensions": [".js", ".jsx", ".ts", ".tsx"] } } -} + } } diff --git a/package.json b/package.json index 0a19812..2d8df37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsksmart/mock-web3-provider", - "version": "0.0.1", + "version": "1.0.0", "main": "dist/index.js", "repository": "git@github.com:jessgusclark/mock-web3-provider.git", "author": "Jesse Clark ", @@ -13,6 +13,7 @@ "build": "npx tsc --outDir ./dist", "build:watch": "npx tsc -w --outDir ./dist", "lint": "npx eslint ./src/*.ts", + "lint:fix": "npx eslint ./src/*.ts --fix", "test": "npx jest", "test:ci": "npx jest --verbose --coverage --watchAll=false --coverageDirectory reports --maxWorkers=2", "test:watch": "npx jest --watch", diff --git a/src/index.test.ts b/src/index.test.ts index 6fb7b67..ef6fd6b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,11 +1,11 @@ -import mockProvider from './index' +import { MockProvider } from './index' -describe('provider', () => { +describe('default provider', () => { const address = '0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D' const privateKey = 'de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3' - const provider = mockProvider({ - address, privateKey, chainId: 31, debug: false + const provider = new MockProvider({ + address, privateKey, networkVersion: 31, debug: false }) it('returns a provider object', () => { @@ -70,3 +70,44 @@ describe('provider', () => { }) }) }) + +describe('provider with confirm enable', () => { + const address = '0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D' + const privateKey = 'de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3' + + const provider = new MockProvider({ + address, privateKey, networkVersion: 31, debug: false, manualConfirmEnable: true + }) + + it('should not allow to use acceptEnable without pending request', () => { + expect(() => provider.answerEnable(true)).toThrow() + expect(() => provider.answerEnable(false)).toThrow() + }) + + it('resolves with acceptance', async () => { + expect.assertions(1) + + const responsePromise = provider.request({ method: 'eth_requestAccounts', params: [] }) + .then((accounts: any) => expect(accounts[0]).toEqual(address)) + + provider.answerEnable(true) + await responsePromise + }) + + it('rejects with denial', async () => { + expect.assertions(1) + + const responsePromise = provider.request({ method: 'eth_requestAccounts', params: [] }) + .catch((e) => expect(e).toBeDefined()) + + provider.answerEnable(false) + await responsePromise + }) + + /* + it('does not resolver request accounts if no answer', async () => { + // see that this timeouts + await provider.request({ method: 'eth_requestAccounts', params: [] }) + }) + */ +}) diff --git a/src/index.ts b/src/index.ts index 7c815f1..ec7c79b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,81 +1,129 @@ import { personalSign, decrypt } from 'eth-sig-util' -interface ProviderSetup { - address: string, - privateKey: string, - chainId: number, +type ProviderSetup = { + address: string + privateKey: string + networkVersion: number debug?: boolean + manualConfirmEnable?: boolean } -const provider = (startProps: ProviderSetup) => { - const { - address, privateKey, chainId, debug - } = startProps +interface IMockProvider { + request(args: { method: 'eth_accounts'; params: string[] }): Promise + request(args: { method: 'eth_requestAccounts'; params: string[] }): Promise + + request(args: { method: 'net_version' }): Promise + request(args: { method: 'eth_chainId'; params: string[] }): Promise + + request(args: { method: 'personal_sign'; params: string[] }): Promise + request(args: { method: 'eth_decrypt'; params: string[] }): Promise + + request(args: { method: string, params?: any[] }): Promise +} + +// eslint-disable-next-line import/prefer-default-export +export class MockProvider implements IMockProvider { + private setup: ProviderSetup + + private acceptEnable?: (value: unknown) => void + + private rejectEnable?: (value: unknown) => void + + constructor(setup: ProviderSetup) { + this.setup = setup + } - /* Logging */ // eslint-disable-next-line no-console - const log = (...args: (any | null)[]) => debug && console.log('🦄', ...args) - - const buildProvider = { - isMetaMask: true, - networkVersion: chainId, - chainId: `0x${chainId.toString(16)}`, - selectedAddress: address, - - request(props: { method: any; params: string[] }) { - log(`request[${props.method}]`) - switch (props.method) { - case 'eth_requestAccounts': - case 'eth_accounts': - return Promise.resolve([this.selectedAddress]) - case 'net_version': - return Promise.resolve(this.networkVersion) - case 'eth_chainId': - return Promise.resolve(this.chainId) - - case 'personal_sign': { - const privKey = Buffer.from(privateKey, 'hex'); - const signed = personalSign(privKey, { data: props.params[0] }) - log('signed', signed) - return Promise.resolve(signed) - } - case 'eth_sendTransaction': { - return Promise.reject(new Error('This service can not send transactions.')) - } - case 'eth_decrypt': { - log('eth_decrypt', props) - const stripped = props.params[0].substring(2) - const buff = Buffer.from(stripped, 'hex'); - const data = JSON.parse(buff.toString('utf8')); - return Promise.resolve(decrypt(data, privateKey)) + private log = (...args: (any | null)[]) => this.setup.debug && console.log('🦄', ...args) + + get selectedAddress(): string { + return this.setup.address + } + + get networkVersion(): number { + return this.setup.networkVersion + } + + get chainId(): string { + return `0x${this.setup.networkVersion.toString(16)}` + } + + answerEnable(acceptance: boolean) { + if (acceptance) this.acceptEnable!('Accepted') + else this.rejectEnable!('User rejected') + } + + request({ method, params }: any): Promise { + this.log(`request[${method}]`) + + switch (method) { + case 'eth_requestAccounts': + case 'eth_accounts': + if (this.setup.manualConfirmEnable) { + return new Promise((resolve, reject) => { + this.acceptEnable = resolve + this.rejectEnable = reject + }).then(() => [this.selectedAddress]) } - default: - log(`resquesting missing method ${props.method}`) - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject(`The method ${props.method} is not implemented by the mock provider.`) + return Promise.resolve([this.selectedAddress]) + + case 'net_version': + return Promise.resolve(this.setup.networkVersion) + + case 'eth_chainId': + return Promise.resolve(this.chainId) + + case 'personal_sign': { + const privKey = Buffer.from(this.setup.privateKey, 'hex'); + + const signed: string = personalSign(privKey, { data: params[0] }) + + this.log('signed', signed) + + return Promise.resolve(signed) } - }, - - sendAsync(props: { method: string }, cb: any) { - switch (props.method) { - case 'eth_accounts': - cb(null, { result: [this.selectedAddress] }) - break; - case 'net_version': cb(null, { result: this.networkVersion }) - break; - default: log(`Method '${props.method}' is not supported yet.`) + + case 'eth_sendTransaction': { + return Promise.reject(new Error('This service can not send transactions.')) } - }, - on(props: string) { - log('registering event:', props) - }, - removeAllListeners() { - log('removeAllListeners', null) - }, + + case 'eth_decrypt': { + this.log('eth_decrypt', { method, params }) + + const stripped = params[0].substring(2) + const buff = Buffer.from(stripped, 'hex'); + const data = JSON.parse(buff.toString('utf8')); + + const decrypted: string = decrypt(data, this.setup.privateKey) + + return Promise.resolve(decrypted) + } + + default: + this.log(`resquesting missing method ${method}`) + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject(`The method ${method} is not implemented by the mock provider.`) + } } - log('Mock Provider ', buildProvider) - return buildProvider; -} + sendAsync(props: { method: string }, cb: any) { + switch (props.method) { + case 'eth_accounts': + cb(null, { result: [this.setup.address] }) + break; + + case 'net_version': cb(null, { result: this.setup.networkVersion }) + break; + + default: this.log(`Method '${props.method}' is not supported yet.`) + } + } + + on(props: string) { + this.log('registering event:', props) + } -export default provider + removeAllListeners() { + this.log('removeAllListeners', null) + } +}