Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set up Prettier and prettify the code #15

Merged
merged 4 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading