Skip to content

Commit

Permalink
feat: ethereum wallet signer utility (#230)
Browse files Browse the repository at this point in the history
  • Loading branch information
AuHau authored Mar 30, 2021
1 parent 1c4192f commit 94bc9f4
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 2 deletions.
60 changes: 59 additions & 1 deletion src/utils/eth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { keccak256, sha3_256 } from 'js-sha3'
import { BrandedString } from '../types'
import { BrandedString, Data } from '../types'
import { HexString, hexToBytes, intToHex, makeHexString, assertHexString } from './hex'
import { Bytes, verifyBytes } from './bytes'
import { Signer } from '../chunk/signer'

export type OverlayAddress = BrandedString<'OverlayAddress'>
export type EthAddress = Bytes<20>
Expand Down Expand Up @@ -167,3 +168,60 @@ export function ethToSwarmAddress(ethAddress: string | HexString | HexEthAddress

return overlayAddress as OverlayAddress
}

interface RequestArguments {
method: string
jsonrpc?: string
params?: unknown[] | Record<string, unknown>
}

export interface JsonRPC {
request?(args: RequestArguments): Promise<unknown>
sendAsync?(args: RequestArguments): Promise<unknown>
}

/**
* Function that takes Ethereum EIP-1193 compatible provider and create an Signer instance that
* uses personal_sign method to sign requested data.
*
* @param provider Injected web3 provider like window.ethereum or other compatible with EIP-1193
* @param ethAddress Optional address of the account which the data should be signed with. If not specified eth_requestAccounts requests is used to get the account address.
*/
export async function makeEthereumWalletSigner(
provider: JsonRPC,
ethAddress?: string | HexString | HexEthAddress,
): Promise<Signer> {
let executorFnc: (args: RequestArguments) => Promise<unknown>

if (typeof provider !== 'object' || provider === null) {
throw new TypeError('We need JsonRPC provider object!')
}

if (provider.request) {
executorFnc = provider.request
} else if (provider.sendAsync) {
executorFnc = provider.sendAsync
} else {
throw new Error('Incompatible interface of given provider!')
}

if (!ethAddress) {
ethAddress = ((await executorFnc({ method: 'eth_requestAccounts' })) as string[])[0]
}

const bytesEthAddress = makeEthAddress(ethAddress)
const hexEthAddress = makeHexEthAddress(ethAddress)

return {
address: bytesEthAddress,
sign: async (data: Data): Promise<string> => {
const result = await executorFnc({
jsonrpc: '2.0',
method: 'personal_sign',
params: ['0x' + data.hex(), '0x' + hexEthAddress],
})

return result as string
},
} as Signer
}
85 changes: 84 additions & 1 deletion test/utils/eth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
/* eslint @typescript-eslint/no-empty-function: 0 */
import { ethToSwarmAddress, fromLittleEndian, isHexEthAddress, toLittleEndian } from '../../src/utils/eth'
import {
makeEthereumWalletSigner,
ethToSwarmAddress,
fromLittleEndian,
isHexEthAddress,
JsonRPC,
toLittleEndian,
} from '../../src/utils/eth'
import { HexString, hexToBytes } from '../../src/utils/hex'
import { wrapBytesWithHelpers } from '../../src/utils/bytes'

describe('eth', () => {
describe('isEthAddress', () => {
Expand Down Expand Up @@ -137,4 +146,78 @@ describe('eth', () => {
}),
)
})

describe('makeEthereumWalletSigner', () => {
const dataToSignBytes = hexToBytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' as HexString)
const dataToSignWithHelpers = wrapBytesWithHelpers(dataToSignBytes)
const expectedSignatureHex = '0x336d24afef78c5883b96ad9a62552a8db3d236105cb059ddd04dc49680869dc16234f6852c277087f025d4114c4fac6b40295ecffd1194a84cdb91bd571769491b' as HexString

it('should detect valid interface', async () => {
await expect(makeEthereumWalletSigner({})).rejects.toThrow()
await expect(makeEthereumWalletSigner(('' as unknown) as JsonRPC)).rejects.toThrow(TypeError)
await expect(makeEthereumWalletSigner((1 as unknown) as JsonRPC)).rejects.toThrow(TypeError)
await expect(makeEthereumWalletSigner((null as unknown) as JsonRPC)).rejects.toThrow(TypeError)
await expect(makeEthereumWalletSigner((undefined as unknown) as JsonRPC)).rejects.toThrow(TypeError)
})

it('should request address if not specified', async () => {
const providerMock = jest.fn()
providerMock.mockReturnValue(['0xf1B07aC6E91A423d9c3c834cc9d938E89E19334a'])

const signer = await makeEthereumWalletSigner({ request: providerMock } as JsonRPC)

expect(signer.address).toEqual(hexToBytes('f1B07aC6E91A423d9c3c834cc9d938E89E19334a'))
expect(providerMock.mock.calls.length).toEqual(1)
expect(providerMock.mock.calls[0][0]).toEqual({ method: 'eth_requestAccounts' })
})

it('should request signature when sign() is called', async () => {
const providerMock = jest.fn()
providerMock.mockReturnValue(expectedSignatureHex)

const signer = await makeEthereumWalletSigner(
{ request: providerMock } as JsonRPC,
'0xf1B07aC6E91A423d9c3c834cc9d938E89E19334a',
)
await expect(signer.sign(dataToSignWithHelpers)).resolves.toEqual(expectedSignatureHex)
expect(providerMock.mock.calls.length).toEqual(1)
expect(providerMock.mock.calls[0][0]).toEqual({
jsonrpc: '2.0',
method: 'personal_sign',
params: [
'0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae',
'0xf1B07aC6E91A423d9c3c834cc9d938E89E19334a',
],
})
})

it('should normalize hex prefix for address', async () => {
const providerMock = jest.fn()
providerMock.mockReturnValue(expectedSignatureHex)

const signer = await makeEthereumWalletSigner(
{ request: providerMock } as JsonRPC,
'f1B07aC6E91A423d9c3c834cc9d938E89E19334a',
)
await expect(signer.sign(dataToSignWithHelpers)).resolves.toEqual(expectedSignatureHex)
expect(providerMock.mock.calls.length).toEqual(1)
expect(providerMock.mock.calls[0][0]).toEqual({
jsonrpc: '2.0',
method: 'personal_sign',
params: [
'0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae',
'0xf1B07aC6E91A423d9c3c834cc9d938E89E19334a',
],
})
})

it('should validate eth address', async () => {
const providerMock = jest.fn()
providerMock.mockReturnValue(expectedSignatureHex)

await expect(
makeEthereumWalletSigner({ request: providerMock } as JsonRPC, '0x307aC6E91A423d9c3c834cc9d938E89E19334a'),
).rejects.toThrow(TypeError)
})
})
})

0 comments on commit 94bc9f4

Please sign in to comment.