Skip to content

Commit

Permalink
Set up Prettier and prettify the code (#15)
Browse files Browse the repository at this point in the history
* Add prettier config to package.json

* Prettify source

* Check prettier on CI

* Prettify yml and md files
  • Loading branch information
cawabunga authored Sep 19, 2023
1 parent 2676760 commit 3c222a5
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 78 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Lint
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 10

strategy:
matrix:
os: [ubuntu-latest]
node-version: [18]

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
# This is important to fetch the changes to the previous commit
fetch-depth: 0

- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'

- name: Install dependencies
run: yarn install

- name: Prettify code
uses: actionsx/prettier@v2
with:
args: --check .
62 changes: 31 additions & 31 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Playwright Tests
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
test:
timeout-minutes: 60
Expand All @@ -16,32 +16,32 @@ jobs:
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'

- name: Install dependencies
run: yarn install

- name: Build library
run: yarn build

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Install Playwright Browsers
run: npx playwright install --with-deps

- name: Run Playwright tests
run: yarn playwright test

- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'

- name: Install dependencies
run: yarn install

- name: Build library
run: yarn build

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Install Playwright Browsers
run: npx playwright install --with-deps

- name: Run Playwright tests
run: yarn playwright test

- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 80,
"semi": false,
"singleQuote": true
}
46 changes: 29 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ npm i -D headless-web3-provider
```

## About

The `headless-web3-provider` library emulates a Web3 wallet similar to Metamask and provides programmatic control over various operations, such as switching networks, connecting a wallet, and sending transactions, making it useful for end-to-end testing of Ethereum-based applications. It allows to programmatically accept or decline operations, making it handy for test automation.

#### Supported methods

| Method | Confirmable |
|----------------------------|-------------|
| -------------------------- | ----------- |
| eth_requestAccounts | Yes |
| eth_accounts | Yes |
| eth_sendTransaction | Yes |
Expand All @@ -37,14 +38,14 @@ The `headless-web3-provider` library emulates a Web3 wallet similar to Metamask
| eth_chainId | No |
| net_version | No |


## Examples


### Playwright

Below given a simple example. More complex scenarios you can find in [tests/e2e](./tests/e2e) folder.

Setup (add a fixture):

```js
// tests/fixtures.js
import { test as base } from '@playwright/test'
Expand All @@ -56,19 +57,20 @@ export const test = base.extend({

// injectWeb3Provider - function that injects web3 provider instance into the page
injectWeb3Provider: async ({ signers }, use) => {
await use((page, privateKeys = signers) => (
await use((page, privateKeys = signers) =>
injectHeadlessWeb3Provider(
page,
privateKeys, // Private keys that you want to use in tests
31337, // Chain ID - 31337 is common testnet id
privateKeys, // Private keys that you want to use in tests
31337, // Chain ID - 31337 is common testnet id
'http://localhost:8545' // Ethereum client's JSON-RPC URL
)
))
)
},
})
```

Usage:

```js
// tests/e2e/example.spec.js
import { test } from '../fixtures'
Expand All @@ -87,31 +89,42 @@ test('connect the wallet', async ({ page, injectWeb3Provider }) => {

// Verify if the wallet is really connected
await test.expect(page.locator('text=Connected')).toBeVisible()
await test.expect(page.locator('text=0x8b3a08b22d25c60e4b2bfd984e331568eca4c299')).toBeVisible()
await test
.expect(page.locator('text=0x8b3a08b22d25c60e4b2bfd984e331568eca4c299'))
.toBeVisible()
})
```

### Jest

Add a helper script for injecting the ethereum provider instance.

```ts
// tests/web3-helper.ts
import { Wallet } from 'ethers'
import { makeHeadlessWeb3Provider, Web3ProviderBackend } from 'headless-web3-provider'
import {
makeHeadlessWeb3Provider,
Web3ProviderBackend,
} from 'headless-web3-provider'

/**
* injectWeb3Provider - Function to create and inject web3 provider instance into the global window object
*
* @returns {Array} An array containing the wallets and the web3Provider instance
*/
export function injectWeb3Provider(): [[Wallet, ...Wallet[]], Web3ProviderBackend] {

export function injectWeb3Provider(): [
[Wallet, ...Wallet[]],
Web3ProviderBackend
] {
// Create 2 random instances of Wallet class
const wallets = Array(2).fill(0).map(() => Wallet.createRandom()) as [Wallet, Wallet]
const wallets = Array(2)
.fill(0)
.map(() => Wallet.createRandom()) as [Wallet, Wallet]

// Create an instance of the Web3ProviderBackend class
let web3Manager: Web3ProviderBackend = makeHeadlessWeb3Provider(
wallets.map((wallet) => wallet.privateKey),
31337, // Chain ID - 31337 or is a common testnet id
31337, // Chain ID - 31337 or is a common testnet id
'http://localhost:8545' // Ethereum client's JSON-RPC URL
)

Expand Down Expand Up @@ -146,8 +159,9 @@ describe('<AccountConnect />', () => {
render(<AccountConnect />)

// Request connecting the wallet
await userEvent.click(screen.getByRole('button', { name: /connect wallet/i }))

await userEvent.click(
screen.getByRole('button', { name: /connect wallet/i })
)

// Verify if the wallet is NOT yet connected
expect(screen.queryByText(wallets[0].address)).not.toBeInTheDocument()
Expand All @@ -163,14 +177,12 @@ describe('<AccountConnect />', () => {
})
```


## Additional Tools

Enhance your testing environment with these complementary tools that integrate seamlessly with `headless-web3-provider`:

- [Foundry Anvil](https://book.getfoundry.sh/anvil/) - a dev chain platform ideal for testing your applications against.


## Resources

- [Metamask Test DApp](https://metamask.github.io/test-dapp/)
Expand Down
54 changes: 40 additions & 14 deletions src/Web3ProviderBackend.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'node:assert/strict'
import {
filter,
firstValueFrom,
Expand All @@ -9,7 +10,6 @@ import {
} from 'rxjs'
import { ethers, Wallet } from 'ethers'
import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util'
import assert from 'assert/strict'

import { Web3RequestKind } from './utils'
import {
Expand Down Expand Up @@ -111,7 +111,9 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider {
{ method, params },
async () => {
const accounts = await Promise.all(
this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase())
this.#wallets.map(async (wallet) =>
(await wallet.getAddress()).toLowerCase()
)
)
this.emit('accountsChanged', accounts)
return accounts
Expand All @@ -123,7 +125,9 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider {
case 'eth_accounts': {
if (this._authorizedRequests['eth_requestAccounts']) {
return await Promise.all(
this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase())
this.#wallets.map(async (wallet) =>
(await wallet.getAddress()).toLowerCase()
)
)
}
return []
Expand Down Expand Up @@ -178,7 +182,9 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider {

return this.waitAuthorization({ method, params }, async () => {
const accounts = await Promise.all(
this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase())
this.#wallets.map(async (wallet) =>
(await wallet.getAddress()).toLowerCase()
)
)
this.emit('accountsChanged', accounts)
return [{ parentCapability: 'eth_accounts' }]
Expand Down Expand Up @@ -259,7 +265,7 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider {
waitAuthorization<T>(
requestInfo: PendingRequest['requestInfo'],
task: () => Promise<T>,
permanentPermission = false,
permanentPermission = false
) {
if (this._authorizedRequests[requestInfo.method]) {
return task()
Expand Down Expand Up @@ -349,7 +355,11 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider {
this.#wallets = privateKeys.map((key) => new ethers.Wallet(key))
this.emit(
'accountsChanged',
await Promise.all(this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase()))
await Promise.all(
this.#wallets.map(async (wallet) =>
(await wallet.getAddress()).toLowerCase()
)
)
)
}

Expand Down Expand Up @@ -409,31 +419,47 @@ function without<T>(list: T[], item: T): T[] {

// Allowed keys for a JSON-RPC transaction as defined in:
// https://ethereum.github.io/execution-apis/api-documentation/
const allowedTransactionKeys = ['accessList', 'chainId', 'data', 'from', 'gas', 'gasPrice', 'maxFeePerGas',
'maxPriorityFeePerGas', 'nonce', 'to', 'type', 'value']
const allowedTransactionKeys = [
'accessList',
'chainId',
'data',
'from',
'gas',
'gasPrice',
'maxFeePerGas',
'maxPriorityFeePerGas',
'nonce',
'to',
'type',
'value',
]

// Convert a JSON-RPC transaction to an ethers.js transaction.
// The reverse of this function can be found in the ethers.js library:
// https://github.com/ethers-io/ethers.js/blob/v5.7.2/packages/providers/src.ts/json-rpc-provider.ts#L701
function convertJsonRpcTxToEthersTxRequest(tx: { [key: string]: any }): ethers.providers.TransactionRequest {
function convertJsonRpcTxToEthersTxRequest(tx: {
[key: string]: any
}): ethers.providers.TransactionRequest {
const result: any = {}

allowedTransactionKeys.forEach((key) => {
if (tx[key] == null) { return }
if (tx[key] == null) {
return
}

switch (key) {
// gasLimit is referred to as "gas" in JSON-RPC
case "gas":
case 'gas':
result['gasLimit'] = tx[key]
return
// ethers.js expects `chainId` and `type` to be a number
case "chainId":
case "type":
case 'chainId':
case 'type':
result[key] = Number(tx[key])
return
default:
result[key] = tx[key]
}
});
})
return result
}
10 changes: 6 additions & 4 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export function makeHeadlessWeb3Provider(
method: T,
...args: IWeb3Provider[T] extends Fn ? Parameters<IWeb3Provider[T]> : []
) => Promise<void> = async () => {},
config?: Web3ProviderConfig
config?: Web3ProviderConfig
) {
const chainRpc = new ethers.providers.JsonRpcProvider(rpcUrl, chainId)
const web3Provider = new Web3ProviderBackend(privateKeys, [
{ chainId, rpcUrl },
], config)
const web3Provider = new Web3ProviderBackend(
privateKeys,
[{ chainId, rpcUrl }],
config
)

relayEvents(web3Provider, evaluate)

Expand Down
Loading

0 comments on commit 3c222a5

Please sign in to comment.