Skip to content

Commit b0679f6

Browse files
authored
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
1 parent 8a8d28d commit b0679f6

File tree

4 files changed

+167
-75
lines changed

4 files changed

+167
-75
lines changed

.eslintrc.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
"radix": "off",
2323
"import/extensions": ["error", "always", {
2424
"ts": "never"
25-
}]
25+
}],
26+
"no-unused-vars": "off", // ref: https://github.com/typescript-eslint/typescript-eslint/blob/b5b5f415c234f3456575a69da31ac9f6d2f8b146/packages/eslint-plugin/docs/rules/no-unused-vars.md
27+
"@typescript-eslint/no-unused-vars": ["error"]
2628
},
2729
"settings": {
2830
"import/resolver": {
2931
"node": {
3032
"extensions": [".js", ".jsx", ".ts", ".tsx"]
3133
}
3234
}
33-
}
35+
}
3436
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rsksmart/mock-web3-provider",
3-
"version": "0.0.1",
3+
"version": "1.0.0",
44
"main": "dist/index.js",
55
"repository": "git@github.com:jessgusclark/mock-web3-provider.git",
66
"author": "Jesse Clark <hello@developerjesse.com>",
@@ -13,6 +13,7 @@
1313
"build": "npx tsc --outDir ./dist",
1414
"build:watch": "npx tsc -w --outDir ./dist",
1515
"lint": "npx eslint ./src/*.ts",
16+
"lint:fix": "npx eslint ./src/*.ts --fix",
1617
"test": "npx jest",
1718
"test:ci": "npx jest --verbose --coverage --watchAll=false --coverageDirectory reports --maxWorkers=2",
1819
"test:watch": "npx jest --watch",

src/index.test.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import mockProvider from './index'
1+
import { MockProvider } from './index'
22

3-
describe('provider', () => {
3+
describe('default provider', () => {
44
const address = '0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D'
55
const privateKey = 'de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3'
66

7-
const provider = mockProvider({
8-
address, privateKey, chainId: 31, debug: false
7+
const provider = new MockProvider({
8+
address, privateKey, networkVersion: 31, debug: false
99
})
1010

1111
it('returns a provider object', () => {
@@ -70,3 +70,44 @@ describe('provider', () => {
7070
})
7171
})
7272
})
73+
74+
describe('provider with confirm enable', () => {
75+
const address = '0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D'
76+
const privateKey = 'de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3'
77+
78+
const provider = new MockProvider({
79+
address, privateKey, networkVersion: 31, debug: false, manualConfirmEnable: true
80+
})
81+
82+
it('should not allow to use acceptEnable without pending request', () => {
83+
expect(() => provider.answerEnable(true)).toThrow()
84+
expect(() => provider.answerEnable(false)).toThrow()
85+
})
86+
87+
it('resolves with acceptance', async () => {
88+
expect.assertions(1)
89+
90+
const responsePromise = provider.request({ method: 'eth_requestAccounts', params: [] })
91+
.then((accounts: any) => expect(accounts[0]).toEqual(address))
92+
93+
provider.answerEnable(true)
94+
await responsePromise
95+
})
96+
97+
it('rejects with denial', async () => {
98+
expect.assertions(1)
99+
100+
const responsePromise = provider.request({ method: 'eth_requestAccounts', params: [] })
101+
.catch((e) => expect(e).toBeDefined())
102+
103+
provider.answerEnable(false)
104+
await responsePromise
105+
})
106+
107+
/*
108+
it('does not resolver request accounts if no answer', async () => {
109+
// see that this timeouts
110+
await provider.request({ method: 'eth_requestAccounts', params: [] })
111+
})
112+
*/
113+
})

src/index.ts

Lines changed: 116 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,129 @@
11
import { personalSign, decrypt } from 'eth-sig-util'
22

3-
interface ProviderSetup {
4-
address: string,
5-
privateKey: string,
6-
chainId: number,
3+
type ProviderSetup = {
4+
address: string
5+
privateKey: string
6+
networkVersion: number
77
debug?: boolean
8+
manualConfirmEnable?: boolean
89
}
910

10-
const provider = (startProps: ProviderSetup) => {
11-
const {
12-
address, privateKey, chainId, debug
13-
} = startProps
11+
interface IMockProvider {
12+
request(args: { method: 'eth_accounts'; params: string[] }): Promise<string[]>
13+
request(args: { method: 'eth_requestAccounts'; params: string[] }): Promise<string[]>
14+
15+
request(args: { method: 'net_version' }): Promise<number>
16+
request(args: { method: 'eth_chainId'; params: string[] }): Promise<string>
17+
18+
request(args: { method: 'personal_sign'; params: string[] }): Promise<string>
19+
request(args: { method: 'eth_decrypt'; params: string[] }): Promise<string>
20+
21+
request(args: { method: string, params?: any[] }): Promise<any>
22+
}
23+
24+
// eslint-disable-next-line import/prefer-default-export
25+
export class MockProvider implements IMockProvider {
26+
private setup: ProviderSetup
27+
28+
private acceptEnable?: (value: unknown) => void
29+
30+
private rejectEnable?: (value: unknown) => void
31+
32+
constructor(setup: ProviderSetup) {
33+
this.setup = setup
34+
}
1435

15-
/* Logging */
1636
// eslint-disable-next-line no-console
17-
const log = (...args: (any | null)[]) => debug && console.log('🦄', ...args)
18-
19-
const buildProvider = {
20-
isMetaMask: true,
21-
networkVersion: chainId,
22-
chainId: `0x${chainId.toString(16)}`,
23-
selectedAddress: address,
24-
25-
request(props: { method: any; params: string[] }) {
26-
log(`request[${props.method}]`)
27-
switch (props.method) {
28-
case 'eth_requestAccounts':
29-
case 'eth_accounts':
30-
return Promise.resolve([this.selectedAddress])
31-
case 'net_version':
32-
return Promise.resolve(this.networkVersion)
33-
case 'eth_chainId':
34-
return Promise.resolve(this.chainId)
35-
36-
case 'personal_sign': {
37-
const privKey = Buffer.from(privateKey, 'hex');
38-
const signed = personalSign(privKey, { data: props.params[0] })
39-
log('signed', signed)
40-
return Promise.resolve(signed)
41-
}
42-
case 'eth_sendTransaction': {
43-
return Promise.reject(new Error('This service can not send transactions.'))
44-
}
45-
case 'eth_decrypt': {
46-
log('eth_decrypt', props)
47-
const stripped = props.params[0].substring(2)
48-
const buff = Buffer.from(stripped, 'hex');
49-
const data = JSON.parse(buff.toString('utf8'));
50-
return Promise.resolve(decrypt(data, privateKey))
37+
private log = (...args: (any | null)[]) => this.setup.debug && console.log('🦄', ...args)
38+
39+
get selectedAddress(): string {
40+
return this.setup.address
41+
}
42+
43+
get networkVersion(): number {
44+
return this.setup.networkVersion
45+
}
46+
47+
get chainId(): string {
48+
return `0x${this.setup.networkVersion.toString(16)}`
49+
}
50+
51+
answerEnable(acceptance: boolean) {
52+
if (acceptance) this.acceptEnable!('Accepted')
53+
else this.rejectEnable!('User rejected')
54+
}
55+
56+
request({ method, params }: any): Promise<any> {
57+
this.log(`request[${method}]`)
58+
59+
switch (method) {
60+
case 'eth_requestAccounts':
61+
case 'eth_accounts':
62+
if (this.setup.manualConfirmEnable) {
63+
return new Promise((resolve, reject) => {
64+
this.acceptEnable = resolve
65+
this.rejectEnable = reject
66+
}).then(() => [this.selectedAddress])
5167
}
52-
default:
53-
log(`resquesting missing method ${props.method}`)
54-
// eslint-disable-next-line prefer-promise-reject-errors
55-
return Promise.reject(`The method ${props.method} is not implemented by the mock provider.`)
68+
return Promise.resolve([this.selectedAddress])
69+
70+
case 'net_version':
71+
return Promise.resolve(this.setup.networkVersion)
72+
73+
case 'eth_chainId':
74+
return Promise.resolve(this.chainId)
75+
76+
case 'personal_sign': {
77+
const privKey = Buffer.from(this.setup.privateKey, 'hex');
78+
79+
const signed: string = personalSign(privKey, { data: params[0] })
80+
81+
this.log('signed', signed)
82+
83+
return Promise.resolve(signed)
5684
}
57-
},
58-
59-
sendAsync(props: { method: string }, cb: any) {
60-
switch (props.method) {
61-
case 'eth_accounts':
62-
cb(null, { result: [this.selectedAddress] })
63-
break;
64-
case 'net_version': cb(null, { result: this.networkVersion })
65-
break;
66-
default: log(`Method '${props.method}' is not supported yet.`)
85+
86+
case 'eth_sendTransaction': {
87+
return Promise.reject(new Error('This service can not send transactions.'))
6788
}
68-
},
69-
on(props: string) {
70-
log('registering event:', props)
71-
},
72-
removeAllListeners() {
73-
log('removeAllListeners', null)
74-
},
89+
90+
case 'eth_decrypt': {
91+
this.log('eth_decrypt', { method, params })
92+
93+
const stripped = params[0].substring(2)
94+
const buff = Buffer.from(stripped, 'hex');
95+
const data = JSON.parse(buff.toString('utf8'));
96+
97+
const decrypted: string = decrypt(data, this.setup.privateKey)
98+
99+
return Promise.resolve(decrypted)
100+
}
101+
102+
default:
103+
this.log(`resquesting missing method ${method}`)
104+
// eslint-disable-next-line prefer-promise-reject-errors
105+
return Promise.reject(`The method ${method} is not implemented by the mock provider.`)
106+
}
75107
}
76108

77-
log('Mock Provider ', buildProvider)
78-
return buildProvider;
79-
}
109+
sendAsync(props: { method: string }, cb: any) {
110+
switch (props.method) {
111+
case 'eth_accounts':
112+
cb(null, { result: [this.setup.address] })
113+
break;
114+
115+
case 'net_version': cb(null, { result: this.setup.networkVersion })
116+
break;
117+
118+
default: this.log(`Method '${props.method}' is not supported yet.`)
119+
}
120+
}
121+
122+
on(props: string) {
123+
this.log('registering event:', props)
124+
}
80125

81-
export default provider
126+
removeAllListeners() {
127+
this.log('removeAllListeners', null)
128+
}
129+
}

0 commit comments

Comments
 (0)