Skip to content

Commit

Permalink
Allow the developer to manually accept/reject an eth_requestAccounts …
Browse files Browse the repository at this point in the history
…request (#15)

* Refactor: make mock provider a class

* Allow to set flag to request enable acceptance

* Bump to v1

* Lint
  • Loading branch information
ilanolkies authored Mar 22, 2022
1 parent 8a8d28d commit b0679f6
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 75 deletions.
6 changes: 4 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
"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": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
}
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rsksmart/mock-web3-provider",
"version": "0.0.1",
"version": "1.0.0",
"main": "dist/index.js",
"repository": "[email protected]:jessgusclark/mock-web3-provider.git",
"author": "Jesse Clark <[email protected]>",
Expand All @@ -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",
Expand Down
49 changes: 45 additions & 4 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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: [] })
})
*/
})
184 changes: 116 additions & 68 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>
request(args: { method: 'eth_requestAccounts'; params: string[] }): Promise<string[]>

request(args: { method: 'net_version' }): Promise<number>
request(args: { method: 'eth_chainId'; params: string[] }): Promise<string>

request(args: { method: 'personal_sign'; params: string[] }): Promise<string>
request(args: { method: 'eth_decrypt'; params: string[] }): Promise<string>

request(args: { method: string, params?: any[] }): Promise<any>
}

// 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<any> {
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)
}
}

0 comments on commit b0679f6

Please sign in to comment.